Compare commits

..

203 commits

Author SHA1 Message Date
K Muller
5634d4d232 Merge branch 'trunk' 2026-10-15 09:12:44 +02:00
K Muller
9a1a07307b Merge branch 'develop' into trunk 2026-10-15 09:12:16 +02:00
Karl M'muller
8410447a29 Merge branch '134-restore-soft-delete' into 'develop'
feat: ✨ restore soft-deleted user on create

See merge request ldc-web/projets/support-web!138
2026-10-15 07:11:08 +00:00
Corentin Huet
f0fb7a26f3 feat: ✨ restore soft-deleted user on create 2026-10-08 15:31:11 +02:00
Noah Lancien
f55ca959b8 Merge branch 'trunk' 2026-10-07 15:30:35 +02:00
Noah Lancien
d622f543e9 Merge branch 'develop' into trunk 2026-10-07 15:29:51 +02:00
Noix
1f1c684008 Merge branch '133-export-file' into 'develop'
feat: ✨ change ExportTicketsState to Friday 14:00

See merge request ldc-web/projets/support-web!137
2026-10-07 13:20:45 +00:00
Corentin Huet
e8c5472d45 feat: ✨ change ExportTicketsState to Friday 14:00 2026-10-07 09:30:01 +02:00
Noah Lancien
4ff19d9277 Merge branch 'trunk' 2026-10-01 10:07:26 +02:00
Noah Lancien
7dfd80cf23 Merge branch 'develop' into trunk 2026-10-01 10:07:12 +02:00
Noix
e19d810a2f Merge branch '132-create-gitlab-issue' into 'develop'
feat: ✨ allow creating gitlab issues for mailing type

See merge request ldc-web/projets/support-web!136
2026-10-01 08:06:30 +00:00
Corentin Huet
ab998c975d feat: ✨ allow creating gitlab issues for mailing type 2026-09-01 16:51:04 +02:00
Vincent BRIDIER
9706922312 Merge branch 'trunk' 2026-08-27 20:56:21 +02:00
Vincent BRIDIER
ebb52b5968 Merge branch 'develop' into trunk 2026-08-27 20:56:07 +02:00
Vincent BRIDIER
6c783247a7 fix: Alpine www-data = 82 2026-08-27 20:54:40 +02:00
K Muller
6cf790feab Merge branch 'trunk' 2026-08-26 12:58:54 +02:00
K Muller
57c7613455 Merge branch 'develop' into trunk 2026-08-26 12:58:41 +02:00
Karl M'muller
eddeb4f7f6 Merge branch '128-archived-project' into 'develop'
fix: 🐛 add error message for archived project

See merge request ldc-web/projets/support-web!135
2026-08-26 10:58:10 +00:00
Karl M'muller
d9d1fea624 Merge branch '127-error-sentry' into 'develop'
fix: 🐛 update regex to match both issue and work items

See merge request ldc-web/projets/support-web!134
2026-08-26 10:57:57 +00:00
Corentin Huet
22d79cfb11 fix: 🐛 add error message for archived project 2026-08-25 13:16:11 +02:00
Corentin Huet
22bc49baab fix: 🐛 update regex to match both issue and work items 2026-08-24 17:50:16 +02:00
K Muller
adb2a8b21c Merge branch 'trunk' 2026-07-23 09:26:12 +02:00
K Muller
7246d7894d Merge branch 'develop' into trunk 2026-07-23 09:25:13 +02:00
Karl M'muller
10b75031e7 Merge branch 'issue-126' into 'develop'
Fix: 🐛 duplicate emails sent when a ticket's modif is changed

See merge request ldc-web/projets/support-web!133
2026-07-23 07:23:38 +00:00
Abdel LAMRANI
68a48fdf5a Fix: 🐛 duplicate emails sent when a ticket's modif is changed 2026-07-20 12:45:46 +02:00
Jeremy Munsch
a7debacd65
Merge branch 'trunk' 2026-06-26 15:36:53 +02:00
Jeremy Munsch
dd10d4ba3a
Merge branch 'develop' into trunk 2026-06-26 15:36:43 +02:00
Jeremy Munsch
a7bd6d6420
fixed: NotifyAdmin event listener 2026-06-26 15:36:34 +02:00
Jeremy Munsch
8cfc409d5c
Merge branch 'trunk' 2026-06-26 15:28:54 +02:00
Jeremy Munsch
bc7a1020bc
Merge branch 'develop' into trunk 2026-06-26 15:28:45 +02:00
Jeremy Munsch
e828c8bf66
fix: TicketCreated event 2026-06-26 15:28:31 +02:00
Jeremy Munsch
f28dbdd1df
Merge branch 'trunk' 2026-06-26 15:15:10 +02:00
Jeremy Munsch
965226fae6
Fixed ticket creation 2026-06-26 15:14:32 +02:00
Jeremy Munsch
bd9b7cd32e
fix: new ticket creation failed 2026-06-26 15:13:48 +02:00
Noah Lancien
8c67989526 Merge branch 'trunk' 2026-06-26 09:50:56 +02:00
Noah Lancien
2212b0ae08 Merge branch 'develop' into trunk 2026-06-26 09:50:41 +02:00
Noix
c2b25c3503 Merge branch 'brevo-fix' into 'develop'
fix: brevo in (content commit that were missed)

See merge request ldc-web/projets/support-web!132
2026-06-26 07:46:25 +00:00
Jeremy Munsch
f106c17588
fix: brevo in (content commit that were missed) 2026-06-26 09:33:50 +02:00
Noah Lancien
8e9d117918 Merge branch 'trunk' 2026-06-25 11:07:30 +02:00
Noah Lancien
0dbb483fc4 Merge branch 'develop' into trunk 2026-06-25 08:47:02 +02:00
Noah Lancien
980717f37c ci: swicth to docker multi-stage 2026-06-25 08:46:44 +02:00
Noah Lancien
020fcdaa03 Merge branch 'develop' into trunk 2026-06-25 08:37:04 +02:00
Noix
095f80eac1 Merge branch 'brevo-webhook-fix' into 'develop'
Brevo webhook fix Closes #106

See merge request ldc-web/projets/support-web!130
2026-06-25 06:36:32 +00:00
Noix
6a973703c4 Merge branch 'issue-124' into 'develop'
refactor: change ldc logo Closes #124

See merge request ldc-web/projets/support-web!131
2026-06-25 06:36:22 +00:00
Jeremy Munsch
9a853088b1
refactor: change ldc logo Closes #124 2026-06-24 17:22:58 +02:00
Jeremy Munsch
0ec3be521d
refactor: Fixes #106 create ticket automatically from brevo webhook manager 2026-06-24 16:46:12 +02:00
Jeremy Munsch
6854af708b
fix: fix project migrations 2026-06-24 13:22:36 +02:00
Noah Lancien
8f1c920e26 Merge branch 'trunk' 2026-05-18 10:28:44 +02:00
Noah Lancien
efe5e8ace6 Merge branch 'develop' into trunk 2026-05-18 10:28:29 +02:00
Noah Lancien
91f4773c60 chore: 📦 Added meow npm package 2026-05-18 10:28:21 +02:00
Noah Lancien
6b5b4ec619 Merge branch 'trunk' 2026-05-18 10:18:39 +02:00
Noah Lancien
4edd92e572 Merge branch 'develop' into trunk 2026-05-18 10:18:20 +02:00
Noix
da03fbe273 Merge branch '115-project-not-found' into 'develop'
fix: 🐛 handle 404 when gitlab project is not found

See merge request ldc-web/projets/support-web!128
2026-05-18 08:12:44 +00:00
Noah Lancien
b7f7b7c55d Merge branch 'trunk' 2026-04-29 16:28:13 +02:00
Noah Lancien
49d37a8fa6 Merge branch 'develop' into trunk 2026-04-29 16:18:44 +02:00
Noah Lancien
195a5fcb3e chore: arrow_up: Bump PHP 8.5 2026-04-29 16:18:25 +02:00
Noah Lancien
e0af4a243a Merge branch 'trunk' 2026-04-29 16:10:44 +02:00
Noah Lancien
0c4d376c22 Merge branch 'develop' into trunk 2026-04-29 16:10:11 +02:00
Noix
6fabb998ed Merge branch 'support-to-glab-labels' into 'develop'
fix: 🐛 update to-dispatch label to g-to-dispatch

See merge request ldc-web/projets/support-web!129
2026-04-29 14:09:36 +00:00
Corentin Huet
04bd156849 fix: 🐛 update to-dispatch label to g-to-dispatch 2026-04-28 15:16:20 +02:00
Corentin Huet
a43037d2f9 fix: 🐛 handle 404 when gitlab project is not found 2026-03-30 11:59:42 +02:00
Noah Lancien
ef57fe9fe5 Merge branch 'trunk' 2026-03-25 11:52:33 +01:00
Noah Lancien
62901a015e Merge branch 'develop' into trunk 2026-03-25 11:52:18 +01:00
Noah Lancien
cbabb40f83 Merge branch 'develop' into trunk 2026-02-26 12:10:44 +01:00
Noah Lancien
870986faa4 Merge branch 'develop' into trunk 2026-02-26 11:15:14 +01:00
Noah Lancien
33ea75a5ed Merge branch 'trunk' 2026-01-29 12:38:38 +01:00
Noah Lancien
3e09baf6f3 Merge branch 'develop' into trunk 2026-01-29 12:38:20 +01:00
Noah Lancien
d5f1891764 Merge branch 'trunk' 2026-01-19 09:39:57 +01:00
Noah Lancien
098e40576e Merge branch 'develop' into trunk 2026-01-19 09:39:06 +01:00
Noah Lancien
18b66793a0 Merge branch 'trunk' 2026-01-16 15:56:26 +01:00
Noah Lancien
7ee8a2be82 Merge branch 'develop' into trunk 2026-01-16 15:51:31 +01:00
Noah Lancien
87cc2dec86 Merge branch 'trunk' 2026-01-12 09:32:52 +01:00
Noah Lancien
ea41e19724 Merge branch 'develop' into trunk 2026-01-12 09:32:32 +01:00
Vincent BRIDIER
65772aeb1d Merge branch 'trunk' 2026-01-02 15:31:49 +01:00
Vincent BRIDIER
ea6ac998d8 Merge branch 'develop' into trunk 2026-01-02 15:31:35 +01:00
Noah Lancien
9bc3d9e5e0 Merge branch 'trunk' 2025-11-28 09:33:55 +01:00
Noah Lancien
f99dac8aab Merge branch 'develop' into trunk 2025-11-21 09:07:52 +01:00
Noah Lancien
3f4342f3bb Merge branch 'develop' into trunk 2025-11-20 15:54:16 +01:00
Noah Lancien
481c732f3e Merge branch 'trunk' 2025-10-30 12:32:32 +01:00
Noah Lancien
887837dcaf Merge branch 'develop' into trunk 2025-10-30 12:22:06 +01:00
K Muller
4bdcf60dfa Merge branch 'develop' into trunk 2025-10-23 10:32:31 +02:00
Noah Lancien
435d5626ee Merge branch 'trunk' 2025-10-09 11:50:24 +02:00
Noah Lancien
50ba8eaf8a Merge branch 'develop' into trunk 2025-10-09 11:50:12 +02:00
Noah Lancien
f2a65c85b0 Merge branch 'trunk' 2025-10-09 11:43:15 +02:00
Noah Lancien
1a2fe21212 Merge branch 'develop' into trunk 2025-10-09 11:42:45 +02:00
Noah Lancien
d702c02bdb Merge branch 'trunk' 2025-09-18 08:59:45 +02:00
Noah Lancien
b1a6dfb72c Merge branch 'develop' into trunk 2025-09-18 08:58:20 +02:00
Vincent BRIDIER
844928e945 Merge branch 'trunk' 2025-08-22 12:51:32 +02:00
Vincent BRIDIER
3560e6c7f6 Merge branch 'develop' into trunk 2025-08-22 10:28:08 +02:00
Vincent BRIDIER
aa3f507483
Merge branch 'trunk' 2025-08-18 14:16:25 +02:00
Vincent BRIDIER
8a34a30ab4
Merge branch 'develop' into trunk 2025-08-18 14:16:17 +02:00
Vincent BRIDIER
4a0c556937
Merge branch 'trunk' 2025-08-18 14:03:06 +02:00
Vincent BRIDIER
085028a465
Merge branch 'develop' into trunk 2025-08-18 12:40:48 +02:00
Vincent BRIDIER
f9ed83ff60
Merge branch 'develop' into trunk 2025-08-18 12:13:24 +02:00
Noah Lancien
1ffb5e5bdd Merge branch 'trunk' 2025-07-10 14:54:56 +02:00
Noah Lancien
c6fcd7cfa6 Merge branch 'develop' into trunk 2025-07-10 13:05:54 +02:00
Noah Lancien
3d81cd77ce Merge branch 'trunk' 2025-07-09 15:31:20 +02:00
Noah Lancien
fba178b598 Merge branch 'develop' into trunk 2025-07-09 15:23:22 +02:00
Noah Lancien
1f02f86610 Merge branch 'trunk' 2025-06-24 10:31:22 +02:00
Noah Lancien
efb3914e4f Merge branch 'develop' into trunk 2025-06-24 10:31:03 +02:00
Noah Lancien
b0f4c37153 Merge branch 'trunk' 2025-06-19 12:09:03 +02:00
Noah Lancien
25ab578c93 Merge branch 'develop' into trunk 2025-06-19 12:01:08 +02:00
Noah Lancien
52386bbfd4 Merge branch 'develop' into trunk 2025-06-19 11:48:17 +02:00
Noah Lancien
d6ed7c1f04 Merge branch 'trunk' 2025-06-16 12:00:47 +02:00
Noah Lancien
5477affb97 Merge branch 'develop' into trunk 2025-06-16 11:52:26 +02:00
Vincent BRIDIER
3e199d0f68
Merge branch 'trunk' 2025-05-27 10:30:17 +02:00
Vincent BRIDIER
7564600ace
Merge branch 'develop' into trunk 2025-05-27 10:30:05 +02:00
Vincent BRIDIER
ba6f7f8e7b
Merge branch 'trunk' 2025-05-12 15:11:13 +02:00
Vincent BRIDIER
f1eddb1cc2
Merge branch 'develop' into trunk 2025-05-12 15:10:59 +02:00
Vincent BRIDIER
658fedd3eb
Merge branch 'trunk' 2025-05-12 11:42:21 +02:00
Vincent BRIDIER
31510149af
Merge branch 'develop' into trunk 2025-05-12 11:41:46 +02:00
Noah Lancien
fb8720d41b Merge branch 'trunk' 2025-04-24 15:02:27 +02:00
Noah Lancien
08afe201f8 Merge branch 'develop' into trunk 2025-04-24 15:02:07 +02:00
Noah Lancien
afd8f09d02 Merge branch 'trunk' 2025-04-23 12:11:56 +02:00
Noah Lancien
33cf4f1c93 Merge branch 'develop' into trunk 2025-04-23 12:11:30 +02:00
Vincent BRIDIER
e1944924c1
Merge branch 'trunk' 2025-04-18 10:28:51 +02:00
Vincent BRIDIER
3a0cc47fa8
Merge branch 'develop' into trunk 2025-04-18 10:14:27 +02:00
Vincent BRIDIER
3a2b5a0af9
Merge branch 'develop' into trunk 2025-04-18 09:59:53 +02:00
Noah Lancien
ca7057ce77 Merge branch 'develop' into trunk 2025-04-08 14:11:31 +02:00
Noah Lancien
0dd283b68d Merge branch 'trunk' 2025-04-07 15:01:11 +02:00
Noah Lancien
7c028a4505 Merge branch 'develop' into trunk 2025-04-07 15:00:52 +02:00
Karl Muller
b21dd4d260 Merge branch 'trunk' 2025-03-07 14:27:01 +00:00
Karl Muller
4838b54b4c Merge branch 'develop' into trunk 2025-03-07 14:26:49 +00:00
Karl Muller
911ee5df4b Merge branch 'trunk' 2025-03-07 14:10:23 +00:00
Karl Muller
bbfd4360f2 Merge branch 'develop' into trunk 2025-03-07 14:09:58 +00:00
Vincent BRIDIER
3a51ccb0f3
Merge branch 'trunk' 2025-03-04 09:58:05 +01:00
Vincent BRIDIER
1c3f20782b
Merge branch 'develop' into trunk 2025-03-04 09:57:56 +01:00
Noah Lancien
673494aeda Merge branch 'trunk' 2025-02-20 14:41:26 +01:00
Noah Lancien
1bd04e2e52 Merge branch 'develop' into trunk 2025-02-20 14:41:11 +01:00
Noah Lancien
aa17aab45f Merge branch 'trunk' 2025-02-14 13:42:22 +01:00
Noah Lancien
ec47d2610f Merge branch 'develop' into trunk 2025-02-14 13:42:08 +01:00
Noah Lancien
560ef30cd8 Merge branch 'trunk' 2025-02-14 13:31:00 +01:00
Noah Lancien
b2908aac9f Merge branch 'develop' into trunk 2025-02-14 13:30:20 +01:00
Karl Muller
0da4a6fa13 Merge branch 'trunk' 2025-01-13 13:56:41 +00:00
Karl Muller
b6720bae3d Merge branch 'develop' into trunk 2025-01-13 13:46:09 +00:00
Karl Muller
a58bf2ff3b fix: change manifest for cron issue 2025-01-13 13:45:04 +00:00
Noah Lancien
2b94d780b6 Merge branch 'trunk' 2024-11-26 09:18:29 +01:00
Noah Lancien
74edc5c785 Merge branch 'develop' into trunk 2024-11-26 09:17:42 +01:00
Noah Lancien
781161c5e4 Merge branch 'trunk' 2024-11-25 17:19:55 +01:00
Noah Lancien
2d70ff9f29 Merge branch 'develop' into trunk 2024-11-25 17:18:41 +01:00
Noah Lancien
17a01e1f7a Merge branch 'trunk' 2024-11-25 17:12:45 +01:00
Karl Muller
e3d828a9e5 Merge branch 'develop' into trunk 2024-11-25 15:50:32 +00:00
Noah Lancien
d17678342e Merge branch 'trunk' 2024-11-12 14:32:29 +01:00
Noah Lancien
b646cb41fe Merge branch 'develop' into trunk 2024-11-12 14:07:35 +01:00
Noah Lancien
5bd7673a18 Merge branch 'trunk' 2024-07-31 12:21:21 +02:00
Noah Lancien
2faf043f68 Merge branch 'develop' into trunk 2024-07-31 12:12:17 +02:00
Vincent BRIDIER
66c889b02a
Merge branch 'trunk' 2024-07-26 15:33:24 +02:00
Vincent BRIDIER
74600ef3a7
Merge branch 'develop' into trunk 2024-07-26 15:06:54 +02:00
Vincent BRIDIER
35e18112b9
Merge branch 'develop' into trunk 2024-07-25 17:29:35 +02:00
Vincent BRIDIER
f10b46a012
Merge branch 'trunk' 2024-07-20 23:18:59 +02:00
Vincent BRIDIER
523715a3c1
Merge branch 'develop' into trunk 2024-07-20 23:01:04 +02:00
Vincent BRIDIER
c8b4f97c88
Merge branch 'trunk' 2024-07-10 15:23:32 +02:00
Vincent BRIDIER
65b92edc99
Merge branch 'develop' into trunk 2024-07-10 15:23:17 +02:00
Thibault Raquet
2ee245be4c Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/support-web!86
2024-06-27 08:58:58 +00:00
Thibault Raquet
766a990361 Merge branch 'develop' into 'trunk'
Develop

See merge request ldc-web/projets/support-web!85
2024-06-27 08:58:33 +00:00
Vincent BRIDIER
a23509fa09
Merge branch 'trunk' 2024-06-11 10:22:48 +02:00
Vincent BRIDIER
6c8cc25bae
Merge branch 'develop' into trunk 2024-06-11 10:07:26 +02:00
Vincent BRIDIER
3c529a7bc0
Merge branch 'trunk' 2024-02-05 12:17:39 +01:00
Vincent BRIDIER
c8d179e7b3
Merge branch 'develop' into trunk 2024-02-05 12:09:45 +01:00
Vincent BRIDIER
8e4d6d4198
Merge branch 'develop' into trunk 2024-02-02 11:34:37 +01:00
Vincent BRIDIER
a3c2697c86
Merge branch 'trunk' 2024-01-31 18:03:13 +01:00
Vincent BRIDIER
c6d89101b4
Merge branch 'develop' into trunk 2024-01-31 17:55:33 +01:00
Vincent BRIDIER
ad43378b52
Merge branch 'trunk' 2023-10-13 15:23:19 +02:00
Vincent BRIDIER
f40f2e2aa5
Merge branch 'develop' into trunk 2023-10-13 15:16:29 +02:00
Vincent BRIDIER
b55d812aaa Merge branch 'develop' into trunk 2023-10-12 16:55:19 +02:00
Vincent BRIDIER
ab08bad87e Merge branch 'trunk' 2023-10-12 11:47:11 +02:00
Vincent BRIDIER
e8b920a8cd Merge branch 'develop' into trunk 2023-10-12 11:18:37 +02:00
Vincent BRIDIER
cb779c83b8
Merge branch 'trunk' 2023-10-09 15:09:26 +02:00
Vincent BRIDIER
a393b24392
Merge branch 'develop' into trunk 2023-10-04 11:38:28 +02:00
Vincent BRIDIER
cd89f6b443 Merge branch 'trunk' 2023-08-24 16:21:39 +02:00
Vincent BRIDIER
d9b316eae4 Merge branch 'develop' into trunk 2023-08-24 16:01:54 +02:00
Vincent BRIDIER
722369cf61 Merge branch 'develop' into trunk 2023-08-24 12:37:14 +02:00
Vincent BRIDIER
0cc315d2ef Merge branch 'develop' into trunk 2023-08-24 12:35:08 +02:00
Vincent BRIDIER
7559a47044
Merge branch 'trunk' 2023-08-18 12:37:30 +02:00
Vincent BRIDIER
55fef3421e
Merge branch 'develop' into trunk 2023-08-18 12:37:14 +02:00
Vincent BRIDIER
97d8111f4b
Merge branch 'trunk' 2023-08-17 11:21:13 +02:00
Vincent BRIDIER
4354f6a445
Merge branch 'develop' into trunk 2023-08-17 11:18:26 +02:00
Vincent BRIDIER
abdbbae843
Merge branch 'develop' into trunk 2023-08-16 12:24:45 +02:00
Vincent BRIDIER
f150e96d39
Merge branch 'develop' into trunk 2023-08-16 10:30:48 +02:00
Vincent BRIDIER
d2324145a4
Merge branch 'develop' into trunk 2023-08-14 11:24:22 +02:00
Vincent BRIDIER
b6c74efd3d
Merge branch 'trunk' 2023-07-20 15:31:37 +02:00
Vincent BRIDIER
57a437db48
Merge branch 'develop' into trunk 2023-07-20 15:31:24 +02:00
Vincent BRIDIER
f3cb8e7974
Merge branch 'trunk' 2023-06-30 09:14:08 +02:00
Vincent BRIDIER
91c1fe5532
Merge branch 'develop' into trunk 2023-06-30 09:13:52 +02:00
Vincent BRIDIER
29a837ff61
Merge branch 'trunk' 2023-06-29 10:12:31 +02:00
Vincent BRIDIER
bbe4478c09
Merge branch 'develop' into trunk 2023-06-29 10:12:14 +02:00
Vincent BRIDIER
28ee9f593d
Merge branch 'trunk' 2023-06-22 14:58:09 +02:00
Vincent BRIDIER
eb2d4e5267
Merge branch 'develop' into trunk 2023-06-22 14:57:42 +02:00
Vincent BRIDIER
c19d5829e6
Merge branch 'trunk' 2023-04-25 11:35:01 +02:00
Vincent BRIDIER
c1fb743e34
Merge branch 'develop' into trunk 2023-04-25 11:34:24 +02:00
Vincent BRIDIER
1d07c93799
Merge branch 'trunk' 2023-04-18 17:27:35 +02:00
Vincent BRIDIER
32cb11622c
Merge branch 'develop' into trunk 2023-04-18 17:27:21 +02:00
Vincent BRIDIER
911d7a2f6d
Merge branch 'trunk' 2023-04-17 17:21:58 +02:00
Vincent BRIDIER
32be1746b5
Merge branch 'develop' into trunk 2023-04-17 16:22:06 +02:00
Vincent BRIDIER
a5d4c58604
Merge branch 'develop' into trunk 2023-04-12 15:47:13 +02:00
Vincent BRIDIER
0d6f34b05a
Merge branch 'trunk' 2022-12-15 09:10:57 +01:00
Vincent BRIDIER
606ca09608
Merge branch 'develop' into trunk 2022-12-15 09:10:38 +01:00
Jérémy Munsch
6523875860
Merge branch 'trunk' 2022-11-04 17:04:58 +01:00
Jérémy Munsch
83b7e42b90
Merge branch 'develop' into trunk 2022-11-04 17:03:49 +01:00
Jérémy Munsch
6f20a6b8db
Merge branch 'develop' into trunk 2022-10-13 10:15:36 +02:00
Jérémy Munsch
88f3da4c92
Merge branch 'hotfix' 2022-10-12 14:22:46 +02:00
Vincent BRIDIER
bd3133f06c
Merge branch 'trunk' 2022-10-11 12:06:30 +02:00
Vincent BRIDIER
bb2cbc5ad8
Merge branch 'develop' into trunk 2022-10-11 12:06:06 +02:00
894 changed files with 147379 additions and 45809 deletions

5
.browserslistrc Normal file
View file

@ -0,0 +1,5 @@
# Browsers that we support
last 3 version
> 0.3%
not dead # no browsers without security updates

View file

@ -1,10 +1,2 @@
.git/
.gitlab/
.gitlab-ci.yml
*.md
.vscode/
auth.json
Dockerfile
manifest.yaml
node_modules/
production-resources.yaml
.git
*.md

View file

@ -17,8 +17,5 @@ indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.scss]
indent_size = 2
[*.{tsx,ts,cjs,js}]
[*.vue]
indent_size = 2

View file

@ -1,65 +1,47 @@
# APP
APP_NAME=
APP_ENV=
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=
APP_URL=
APP_LOCALE=
APP_FALLBACK_LOCALE=
APP_FAKER_LOCALE=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# DATABASE
DB_CONNECTION=mysql
DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
# MAILS
MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME="${APP_NAME}"
# SENTRY
SENTRY_LARAVEL_DSN=
# ADMIN SECURE
LDC_WEB_API_ENDPOINT=
LDC_WEB_API_KEY=
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=
## TELESCOPE
TELESCOPE_ENABLED=true
# REDIS
REDIS_HOST=
REDIS_PORT=
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_PREFIX=
REDIS_SCHEME=
# OTHERS
LOG_CHANNEL=
LOG_DEPRECATIONS_CHANNEL=
LOG_LEVEL=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
CACHE_STORE=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
VITE_APP_NAME="${APP_NAME}"
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=noreply-sw@ldc.fr
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LIST_SERVICEWEB=digital@ldc.fr
MAIL_ADMIN=digital@ldc.fr
# Sentry
SENTRY_LARAVEL_DSN=
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional)
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=
# GITLAB API
GITLAB_URL=
GITLAB_TOKEN=

55
.eslintrc.json Normal file
View file

@ -0,0 +1,55 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
}
}

50
.gitignore vendored
View file

@ -1,38 +1,28 @@
*.log
.env
.env.backup
.env.example
.env.production
/auth.json
/.npmrc
/bootstrap/ssr
.phpunit.result.cache
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/.vscode
/node_modules
/public/build
/public/cache
/public/hot
/public/sitemap.xml
/public/storage
/public/favicons-back
/public/android-icon-36x36.png
/public/android-icon-48x48.png
/public/android-icon-72x72.png
/public/android-icon-96x96.png
/public/android-icon-144x144.png
/public/android-icon-192x192.png
/public/apple-icon-57x57.png
/public/apple-icon-60x60.png
/public/apple-icon-72x72.png
/public/apple-icon-76x76.png
/public/apple-icon-114x114.png
/public/apple-icon-120x120.png
/public/apple-icon-144x144.png
/public/apple-icon-152x152.png
/public/apple-icon-180x180.png
/public/favicon-16x16.png
/public/favicon-32x32.png
/public/favicon.ico
/public/manifest.json
/public/ms-icon-70x70.png
/public/ms-icon-144x144.png
/public/ms-icon-150x150.png
/public/ms-icon-310x310.png
/storage/*.key
/storage/pail
/vendor
auth.json
docker-compose.override.yml
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
storage/samlidp/*
.npmrc
.zshrc
.npm/
.composer/

View file

@ -1,23 +1,24 @@
include:
- project: "ldc-web/shared-ci"
ref: "main"
file: "lint/lint.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "security/security.yml"
- project: "ldc-web/shared-ci"
ref: "main"
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'lint/lint.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'security/security.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: "build/docker-multistage.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "test/test.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "deploy/deploy.yml"
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'test/test.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'deploy/deploy.yml'
stages:
- lint
- security
- build
- test
- deploy
- lint
- security
- build
- test
- deploy

View file

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

5
.npmrc
View file

@ -1,5 +0,0 @@
@ldc-web:registry=https://lab.s-web.fr/api/v4/packages/npm/
//lab.s-web.fr/api/v4/packages/npm/:_authToken=glpat-Td7iwL19ZszGUMosPyNQ
@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=glpat-Td7iwL19ZszGUMosPyNQ

View file

@ -4,8 +4,13 @@
"stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order"
],
"plugins": ["stylelint-scss"],
"files": ["**/*.css", "**/*.scss"],
"plugins": [
"stylelint-scss"
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
@ -13,12 +18,8 @@
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/packages/*.css",
"**/packages/**/*.css",
"**/packages/*.scss",
"**/packages/**/*.scss",
"**/packages/*.sass",
"**/packages/**/*.sass"
"**/vendor/*.less",
"**/vendor/**/*.less"
],
"customSyntax": "postcss-scss",
"rules": {
@ -28,6 +29,8 @@
"order/properties-alphabetical-order": null,
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [
"always",
{
@ -37,20 +40,6 @@
"inside-block"
]
}
],
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"variants",
"responsive",
"screen",
"layer"
]
}
]
}
}

View file

@ -1,19 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"rvest.vs-code-prettier-eslint",
"onecentlin.laravel-extension-pack",
"johnpapa.vscode-peacock",
"xdebug.php-debug",
"neilbrayfield.php-docblocker",
"bmewburn.vscode-intelephense-client",
"valeryanm.vscode-phpsab",
"syler.sass-indented",
"sibiraj-s.vscode-scss-formatter",
"mrmlnc.vscode-scss",
"stylelint.vscode-stylelint",
"vue.vscode-typescript-vue-plugin",
"wscats.vue",
"misterj.vue-volar-extention-pack"
]
}

48
.vscode/launch.json vendored
View file

@ -1,48 +0,0 @@
{
// Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
// Pointez pour afficher la description des attributs existants.
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}
]
}

43
.vscode/settings.json vendored
View file

@ -1,26 +1,23 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#e6f0f8",
"activityBar.activeBorder": "#1a1d02",
"activityBar.background": "#e6f0f8",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#dc82b4",
"activityBarBadge.foreground": "#15202b",
"sash.hoverBorder": "#e6f0f8",
"statusBar.background": "#bdd7ee",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#94bee4",
"statusBarItem.remoteBackground": "#bdd7ee",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#bdd7ee",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#bdd7ee99",
"titleBar.inactiveForeground": "#15202b99",
"commandCenter.border": "#15202b99"
"activityBar.activeBorder": "#621e41",
"activityBar.activeBackground": "#3371a9",
"activityBar.background": "#3371a9",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#621e41",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#3371a9",
"statusBar.background": "#275782",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#3371a9",
"statusBarItem.remoteBackground": "#275782",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#275782",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#27578299",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.remoteColor": "#BDD7EE",
"intelephense.environment.phpVersion": "8.3.0",
"intelephense.completion.fullyQualifyGlobalConstantsAndFunctions": true,
"intelephense.phpdoc.useFullyQualifiedNames": true
}
"peacock.remoteColor": "#275782",
}

View file

@ -1,12 +1,99 @@
# Changelog
### 25.04.2023 -> 1.4.0
- Add ticket types management
- Fix AutoClose Jobs
- Fix Email notifications on private messages
### 04.2023 -> 1.3.0
- Update Status Enum
- Update TicketStatusChanged event
- Update TicketStatus Changed messages
- Add DutyHours
- Add Informations
### 15.12.2022
- Add Sentry
### 12.10.2022 -> 1.2.3
- Hotfixed regular users could not create tickets anymore
All notable changes to `laravel-project` will be documented in this file.
### 01.07.2022 -> 1.2.2
- Fixed CSP directives
- Fixed multiples bugs
- Fixed View sur crédits
- Fixed file create
- Changed estimation can be 0.125 days
- Fixed files view
- Added ticket link on credit log
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
- Commits respect [Conventionnal commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
### 09.06.2022 -> 1.2.1
- CSP rules enforce
- Bug fixes CSP issues
### 31.05.2022 -> 1.2.0
- Ajout de la fonctionnalitée d'estimations
## **[v1.0.0] - 01/01/2026**
### 26.05.2022 -> 1.1.0
- Correction de regression sur macro planning
- Correction de regression sur droits planning
- Ajout de fonctionnalité Label
- Ajout de fonctionalité assigner un ticket
- Rafactorisation du code ticket
- Refonte de tickets.index
- Ajout CSP report URI
- Remove external JS and CSS dependencies
- Use Vuejs runtime
- Add xss sanitizer
- Normalize Laravel mix + add cleaner
- Update npm deps
- Upgrade bootswatch theme
- ADD Secure HEADERS
### Added
- feat: 🎉 working project
### 23.05.2022 -> 1.0.9
- Correction d'erreurs multiples
- Fonctionnalité macro planning
- Mises à jour des dépendances
### 15.03.2022
- le presse-papier du planning n'est plus vidé après un collage
### 08.03.2022 -> 1.0.8
- passage à 8 slots dans le planning
### 03.01.2022 -> 1.0.7
- nouveau stockage pour le planning
### 01.12 -> 1.0.6
- Ajout d'un nouveau type de Ticket : Demande relative aux Newsletters
### 25.10 -> 1.0.5
- Ajout d'un nouveau type de Ticket : Demande relative aux BR, ceci afin de faire une demande
qui concernerait les bons de réduction
- Correction Plannings
- Correction Crud
- Correction de mails relatifs au ticket pour y inclure le bon type de ticket
### 30.09 -> 1.0.4
- Correction lors de l'apperçu des messages
- Correction lors de message sur affichage (auteur message)
- Modification de la couleur BG sur label statut du ticket
- Modification de l'url précédent sur ticket.show (garde en mémoire le filtre multi projet)
- Modification Tri par défaut sur ticket id DESC
### 29.09 -> 1.0.3
- Correction des avertissements avant cloture des tickets
- Ajout d'un bouton pour les utilisateurs afin de réouvrir un ticket clos
- Ajout de messages de confirmation sur les actions de tickets
### 29.09 -> 1.0.2
- Modification Un ticket qui est marqué comme traité ou en production depuis plus de 15 jours et sans activité sera automatiquement clos et archivé
- Modification Il n'est désomrais plus possible de répondre à un ticket archivé dans touts les cas
- Nettoyage
- Correction des noms qui ne s'affichaient pas correctement sur les tickets
### 01.08 -> 0.0.1
- Ajout d'un algo de charge
- Ajout changement de mot de passe
- Ajout messages http explicites
- Correction couleurs
#### 30.07
- Ajout changelog
- Ajout footer infos
- Correction couleurs

View file

@ -1,4 +1,4 @@
FROM composer:2.8 AS vendor
FROM composer:2.9.2 AS vendor
ARG GITLAB_USER
ARG GITLAB_PASSWORD
@ -23,23 +23,18 @@ ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json postcss.config.js tailwind.config.js tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
COPY --from=vendor /app/vendor/ /app/vendor/
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 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/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs
COPY --chown=${USER}:${USER} ./resources/favicons/back/ /app/srcs/public/favicon-back/
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/

34
IDEAS.md Normal file
View file

@ -0,0 +1,34 @@
Algorithme charge de travail ticket
delais 7 jours 168h
travaillé 5 jours 37h
temps tickets 1 jours 7.4h
personnes 4 (TpP) 3.7h / semaine * prs
délais moyen ticket (Dmt) 0.75h
délais max attente pour 1 ticket (Dam) 7jours
niveaux de charge 4: - faible
- stable
- élevée
- saturation
états tickets(Et) : - nouveaux (non traités)
- en attente (charge pendante)
- traités
Algo : linéaire
TpP : 3.7h
P : 4
Et(nvx) * Dam
Nv Charge = (P * TpP) / (Et(nvx) * Dam) - (P * TpP) / (Et(nvx) * DmT) - (Et(att) * DmT)
Si nv Charge < 0 => saturation

View file

@ -1,33 +1,17 @@
# Table of contents
# UWIL Support
- [Table of contents](#table-of-contents)
- [Frameworks, Platforms and Libraries](#frameworks-platforms-and-libraries)
- [Documentation](#documentation)
- [Changelog](#changelog)
- [Copyright and License](#copyright-and-license)
PROD : support.uwil.fr
# Frameworks, Platforms and Libraries
RE7 : support-web.re7-s-web.fr
<!-- https://github.com/Ileriayo/markdown-badges -->
## Gitlab Intégration
Le token de groupe pour l'intégration est valable 11 mois
[![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)](https://sass-lang.com/)
[![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/)
[![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white)](https://vite.dev/)
[![PHP](https://img.shields.io/badge/php-%23777BB4.svg?style=for-the-badge&logo=php&logoColor=white)](https://www.php.net/)
[![Laravel](https://img.shields.io/badge/laravel-%23FF2D20.svg?style=for-the-badge&logo=laravel&logoColor=white)](https://laravel.com/docs/master)
Il faut le renouveler ici : https://lab.s-web.fr/groups/ldc-web/projets/-/settings/access_tokens et le nommer SW Intégration
# Documentation
Role : Guest
Scope : api
```php
composer i/npm i
```
## TODO
# Changelog
Latest version v1.0.0.
See the [CHANGELOG.md](CHANGELOG.md) file for details.
# Copyright and License
Copyright (c) 2025 and beyond UWIL.
- App\Models\Message:booted(): investigate why this make errors on prod,

4633
_ide_helper.php Normal file

File diff suppressed because it is too large Load diff

56
app/Casts/HtmlColor.php Normal file
View file

@ -0,0 +1,56 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
/**
* Custom cast Html Color
* https://laravel.com/docs/8.x/eloquent-mutators#custom-casts
*/
class HtmlColor implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function get($model, $key, $value, $attributes)
{
return "#{$this->zeropad(dechex($value), 6)}";
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return integer
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
*/
public function set($model, $key, $value, $attributes)
{
return \intval(hexdec(ltrim($value, '#')));
}
/**
* Pad with zeros a numeric string
*
* @param string $num
* @param integer $lim
* @return string
*/
private function zeropad(string $num, int $lim): string
{
return (strlen($num) >= $lim) ? $num : $this->zeropad("0" . $num, $lim);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use App\Jobs\AutoCloseOldTickets as JobsAutoCloseOldTickets;
use Illuminate\Console\Command;
class AutoCloseOldTickets extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tickets:auto-close-old';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the AutoCloseOldTickets job.';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new JobsAutoCloseOldTickets();
$job->handle();
$this->info('Job completed');
return 0;
}
}

View file

@ -0,0 +1,168 @@
<?php
namespace App\Console\Commands;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a user';
/**
* The name of the new user.
*
* @var string
*/
protected $name;
/**
* The email of the new user.
*
* @var string
*/
protected $email;
/**
* The password of the new user.
*
* @var string
*/
protected $password;
/**
* The role of the new user.
*
* @var integer
*/
protected $role;
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$this->name = null;
$this->addName();
$this->email = null;
$this->addEmail();
$this->password = null;
$this->addPassword();
$this->role = null;
$this->addRole();
$user = new User([
'name' => $this->name,
'email' => $this->email,
'password' => $this->password,
'role' => $this->role
]);
$user->saveOrFail();
$this->info('User created.');
return 0;
}
/**
* Add a firstname to the user.
*
* @return void
*/
private function addName(): void
{
while (is_null($this->name)) {
try {
$tmp = $this->ask('Type the wanted user name', 'Admin');
$validator = Validator::make(['name' => $tmp], [
'name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add an email to the user.
*
* @return void
*/
private function addEmail(): void
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'digital@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$this->email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\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
}
/**
* Add a password to the user.
*
* @return void
*/
private function addRole(): void
{
while (is_null($this->role)) {
$tmp = $this->confirm('Is the user an admin?');
$validator = Validator::make(['role' => $tmp], [
'role' => 'required|boolean'
]);
$validator->validate();
$this->role = $tmp ? RoleEnum::admin : RoleEnum::client;
};
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ExportTicketsState as JobsExportTicketsState;
use Illuminate\Console\Command;
class ExportTicketsState extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tickets:export-state';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Export the state of tickets to a CSV file';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new JobsExportTicketsState();
$job->handle();
$this->info('Job completed');
return 0;
}
}

View file

@ -1,159 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Uwil\LaravelBackend\Support\UserRoleRegistry;
/**
* Artisan command to create a support admin user.
*/
class MakeUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
#[\Override]
protected $signature = 'support:make:user';
/**
* The console command description.
*
* @var string
*/
#[\Override]
protected $description = 'Create a new Support user.';
/**
* Execute the console command.
*/
public function handle(): void
{
$this->components->info('Creating a new <comment>Support user</comment>...');
$firstName = $this->askValid(
'first_name',
'First name',
'Admin',
['required', 'min:3', 'max:255'],
);
$lastName = $this->askValid(
'last_name',
'Last name',
'Saïtama',
['required', 'min:3', 'max:255'],
);
$email = $this->askValid(
'email',
'Email',
'equipe.web@ldc.fr',
['required', 'unique:users,email', 'email:rfc,strict,dns,spoof,filter'],
);
$role = $this->askRole();
$password = $this->askPassword();
$user = new User([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'role' => $role->value,
'published' => true,
'published_at' => Date::now(),
]);
$user->saveOrFail();
$user->userPassword()->create([
'password' => bcrypt($password),
]);
$this->components->success("User <comment>{$firstName} {$lastName}</comment> created successfully.");
}
/**
* Ask for a value and validate it, re-prompting on failure.
*
* @param array<int, string> $rules
*/
private function askValid(string $field, string $label, string $default, array $rules): string
{
try {
$value = $this->ask(sprintf('Type the wanted user %s', $label), $default);
$validator = Validator::make([$field => $value], [$field => $rules]);
$validator->validate();
return $value;
} catch (ValidationException $e) {
$this->components->error(
collect($e->errors())->flatten()->first()
);
return $this->askValid($field, $label, $default, $rules);
}
}
/**
* Ask for a role and return the corresponding RoleEnum.
*
* @return object{name: string, value: int|string, label: string}
*/
private function askRole(): object
{
$roles = UserRoleRegistry::all();
$defaultRole = UserRoleRegistry::get('client');
$options = collect($roles)->mapWithKeys(
fn (object $role) => [Str::ucFirst($role->label) => $role]
);
$choice = $this->choice(
'Role',
$options->keys()->all(),
Str::ucFirst($defaultRole->label),
);
$choiceKey = is_string($choice) ? $choice : (string) $choice[0];
return $options->get($choiceKey) ?? $defaultRole;
}
/**
* Ask for a password and confirmation password.
*/
private function askPassword(): string
{
$password = $this->secret('Type the wanted user Password');
$passwordConfirmation = $this->secret('Confirm the password');
try {
Validator::make(
[
'password' => $password,
'password_confirmation' => $passwordConfirmation,
],
[
'password' => [
'required',
'confirmed',
'max:255',
Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised(),
],
]
)->validate();
return $password;
} catch (ValidationException $e) {
$this->components->error(collect($e->errors())->flatten()->first());
return $this->askPassword();
}
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class TestMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:mail-test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send test email to digital@ldc.fr';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('digital@ldc.fr')->subject('Test Email');
});
return 0;
}
}

View file

@ -0,0 +1,334 @@
<?php
namespace App\Console\Commands;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\GitlabIssues;
use App\Models\GitlabLabels;
use App\Models\GitlabMilestones;
use App\Models\GitlabProjects;
use App\Models\Ticket;
use Carbon\Carbon;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UpdateGitlabDatas extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:update-gitlab-datas {--is-cron}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch some Gitlab datas';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$isCron = $this->option('is-cron');
$groupId = 23;
$this->updateProjectsByGroup($groupId);
$this->updateLabelsByGroup($groupId);
$projects = GitlabProjects::all();
foreach ($projects as $project) {
$this->updateMilestonesByProject($project);
$this->updateIssuesByProject($project, $isCron);
}
$this->syncTicketStatus();
}
/**
* updateProjectsByGroup function
*
* @param integer $groupId
* @return void
*/
private function updateProjectsByGroup(int $groupId): void
{
$allProjects = [];
$page = 1;
do {
$projects = GitLab::groups()->projects($groupId, [
'order_by' => 'created_at',
'sort' => 'asc',
'per_page' => 100,
'page' => $page,
'include_subgroups' => true
]);
$allProjects = array_merge($allProjects, $projects);
$page++;
} while (!empty($projects));
$this->info(sprintf(
"Fetched %d projects in group %d.",
count($allProjects),
$groupId
));
foreach ($allProjects as $project) {
GitlabProjects::upsert(
[
'name' => $project['name'],
'url' => $project['web_url'],
'is_archived' => $project['archived'],
'gitlab_id' => $project['id']
],
['gitlab_id']
);
}
$this->info("Gitlab Projects upsert complete.");
}
/**
* updateLabelsByGroup function
*
* @param integer $groupId
* @return void
*/
private function updateLabelsByGroup(int $groupId): void
{
$allLabels = [];
$page = 1;
do {
$labels = GitLab::groups()->labels($groupId, [
'per_page' => 100,
'page' => $page,
]);
$allLabels = array_merge($allLabels, $labels);
$page++;
} while (!empty($labels));
$this->info(sprintf(
"Fetched %d labels in group %d.",
count($allLabels),
$groupId
));
foreach ($allLabels as $project) {
GitlabLabels::upsert(
[
'name' => $project['name'],
'gitlab_id' => $project['id']
],
['gitlab_id']
);
}
$this->info("Gitlab labels upsert complete.");
}
/**
* updateIssuesByProject function
*
* @param GitlabProjects $project
* @param boolean $isCron
* @return void
*/
private function updateIssuesByProject(GitlabProjects $project, bool $isCron = false): void
{
$projectId = $project->gitlab_id;
$allIssues = [];
$page = 1;
do {
$params = [
'order_by' => 'created_at',
'sort' => 'asc',
'per_page' => 100,
'page' => $page,
'state' => 'all',
];
if ($isCron) {
$params = array_merge($params, ['updated_after' => now()->subDay()]);
}
$issues = GitLab::projects()->issues($projectId, $params);
$allIssues = array_merge($allIssues, $issues);
$page++;
} while (!empty($issues));
$this->info(sprintf(
"Fetched %d issues in project %d.",
count($allIssues),
$projectId
));
foreach ($allIssues as $issue) {
GitlabIssues::upsert(
[
'title' => $issue['title'],
'url' => $issue['web_url'],
'estimate_time' => $issue['time_stats']['time_estimate'] ?? 0,
'spent_time' => $issue['time_stats']['total_time_spent'] ?? 0,
'is_closed' => $issue['state'] === 'closed',
'gitlab_id' => $issue['id'],
'gitlab_iid' => $issue['iid'],
'milestone_iid' => $issue['milestone']['iid'] ?? null,
'gitlab_project_id' => $projectId,
'g_created_at' => Carbon::parse($issue['created_at'])->format('Y-m-d'),
'g_updated_at' => Carbon::parse($issue['updated_at'])->format('Y-m-d'),
'g_closed_at' => !is_null($issue['closed_at'])
? Carbon::parse($issue['closed_at'])->format('Y-m-d')
: null,
'g_due_date' => !is_null($issue['due_date'])
? Carbon::parse($issue['due_date'])->format('Y-m-d')
: null
],
['gitlab_id']
);
$issueModel = GitlabIssues::where('gitlab_id', $issue['id'])->firstOrFail();
$labelIds = [];
foreach ($issue['labels'] as $labelName) {
$label = Cache::remember("gitlab_label_{$labelName}", 300, function () use ($labelName) {
return GitlabLabels::where('name', $labelName)->first();
});
if ($label) {
$labelIds[] = $label;
}
}
$issueModel->gitlabLabels()->sync($labelIds);
}//end foreach
$this->info("Gitlab issues upsert complete.");
}
/**
* updateMilestonesByProject function
*
* @param GitlabProjects $project
* @return void
*/
private function updateMilestonesByProject(GitlabProjects $project): void
{
$projectId = $project->gitlab_id;
$allMilestones = [];
$page = 1;
do {
$milestones = [];
try {
$milestones = GitLab::milestones()->all($projectId, [
'per_page' => 100,
'page' => $page
]);
} catch (\Exception $e) {
if ($e->getCode() === 404) {
if ($project->delete()) {
Log::warning(sprintf(
"Project not found (%s), it has been deleted.",
$projectId
));
} else {
Log::warning(sprintf(
"Project not found (%s).",
$projectId
));
}
} else {
Log::error(sprintf(
'API Error for project %s, Message: %d',
$projectId,
$e->getMessage()
));
report($e);
}
}
$allMilestones = array_merge($allMilestones, $milestones);
$page++;
} while (!empty($milestones));
$this->info(sprintf(
"Fetched %d milestones in project %d.",
count($allMilestones),
$projectId
));
foreach ($allMilestones as $milestone) {
GitlabMilestones::upsert(
[
'title' => $milestone['title'],
'url' => $milestone['web_url'],
'is_closed' => $milestone['state'] === 'closed',
'gitlab_id' => $milestone['id'],
'gitlab_iid' => $milestone['iid'],
'gitlab_project_id' => $projectId,
'g_created_at' => Carbon::parse($milestone['created_at'])->format('Y-m-d'),
'g_updated_at' => Carbon::parse($milestone['updated_at'])->format('Y-m-d'),
'g_start_date' => !is_null($milestone['start_date'])
? Carbon::parse($milestone['start_date'])->format('Y-m-d')
: null,
'g_due_date' => !is_null($milestone['due_date'])
? Carbon::parse($milestone['due_date'])->format('Y-m-d')
: null
],
['gitlab_id']
);
}//end foreach
$this->info("Gitlab milestones upsert complete.");
}
/**
* Synchronizes ticket statuses based on GitLab issue labels.
*
* @return void
*/
private function syncTicketStatus(): void
{
$tickets = Ticket::whereNotNull('gitlab_issue')->whereNot('status', TicketStatusEnum::archived)->get();
foreach ($tickets as $ticket) {
$gitlabIssue = GitlabIssues::where('url', $ticket->gitlab_issue)->with('gitlabLabels')->first();
$gitlabIssueLabels = $gitlabIssue ? $gitlabIssue->gitlabLabels->pluck('name') : [];
if ($gitlabIssueLabels) {
match (true) {
$gitlabIssueLabels->contains('g-wip') => $this->updateTicketStatus($ticket, TicketStatusEnum::inDev),
$gitlabIssueLabels->contains('g-production') => $this->updateTicketStatus($ticket, TicketStatusEnum::production),
default => null,
};
}
}
}
/**
* Updates the ticket status only if it has changed.
*
* @param Ticket $ticket The ticket to update.
* @param TicketStatusEnum $status The new status to apply.
*
* @return void
*/
private function updateTicketStatus(Ticket $ticket, TicketStatusEnum $status): void
{
if ($ticket->status !== $status && $ticket->status !== TicketStatusEnum::waiting) {
$ticket->status = $status;
$ticket->saveOrFail();
}
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use App\Jobs\WarnBeforeAutoCloseTicket as JobsWarnBeforeAutoCloseTicket;
use Illuminate\Console\Command;
class WarnBeforeAutoCloseTicket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tickets:warn-before-auto-close';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the WarnBeforeAutoCloseTicket job.';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new JobsWarnBeforeAutoCloseTicket();
$job->handle();
$this->info('Job completed');
return 0;
}
}

44
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace App\Console;
use App\Jobs\AutoCloseOldTickets;
use App\Jobs\ExportTicketsState;
use App\Jobs\WarnBeforeAutoCloseTicket;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->job(ExportTicketsState::class)->weekly()->fridays()->at('14:00');
$schedule->job(AutoCloseOldTickets::class)->dailyAt('08:00');
$schedule->job(WarnBeforeAutoCloseTicket::class)->dailyAt('08:05');
$schedule->command('app:update-gitlab-datas --is-cron')->hourly()->between('7:00', '19:00');
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Dto;
final readonly class BrevoWebHookDto
{
public function __construct(
public readonly string $plainTextContent,
public readonly ?string $brand
) {}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Credit;
use App\Traits\Enums\BaseEnum;
enum AccountTypeEnum: int
{
use BaseEnum;
case development = 0;
case seo = 1;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::development->name => "Jours de développement",
self::seo->name => "Jours de seo",
];
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Credit;
use App\Traits\Enums\BaseEnum;
enum BalanceOperationEnum: int
{
use BaseEnum;
case credit = 0;
case debit = 1;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::credit->name => "Créditer un compte",
self::debit->name => "Débiter un compte",
];
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums\Credit;
use App\Traits\Enums\BaseEnum;
enum EstimationStatusEnum: int
{
use BaseEnum;
case pending = 0;
case accepted = 1;
case refused = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::pending->name => "En attente",
self::accepted->name => "Acceptée",
self::refused->name => "Refusée",
];
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Enums\Credit;
use App\Traits\Enums\BaseEnum;
enum EstimationTimeEnum: int
{
use BaseEnum;
case halfHour = 0.0625;
case hour = 0.125;
case hourAndHalf = 0.1875;
case twoHours = 0.25;
case twoHoursAndHalf = 0.3125;
case threeHours = 0.375;
case threeHoursAndHalf = 0.4375;
case fourHours = 0.5;
case fourHoursAndHalf = 0.5625;
case fiveHours = 0.625;
case fiveHoursAndHalf = 0.6875;
case sixHours = 0.75;
case sixHoursAndHalf = 0.8125;
case sevenHours = 0.875;
case sevenHoursAndHalf = 0.9375;
case eightHours = 1;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::halfHour->name => "Une demi-heure",
self::hour->name => "Une heure",
self::hourAndHalf->name => "Une heure et demie",
self::twoHours->name => "Deux heures",
self::twoHoursAndHalf->name => "Deux heures et demie",
self::threeHours->name => "Trois heures",
self::twoHoursAndHalf->name => "Trois heures et demie",
self::fourHours->name => "Quatre heures",
self::fourHoursAndHalf->name => "Quatre heures et demie",
self::fiveHours->name => "Cinq heures",
self::fiveHoursAndHalf->name => "Cinq heures et demie",
self::sixHours->name => "Six heures",
self::sixHoursAndHalf->name => "Six heures et demie",
self::sevenHours->name => "Sept heures",
self::sevenHoursAndHalf->name => "Sept heures et demi",
self::eightHours->name => "Huit heures",
];
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Enums\Informations;
use App\Traits\Enums\BaseEnum;
enum InfoColorsEnum: int
{
use BaseEnum;
case primary = 0;
case secondary = 1;
case success = 2;
case danger = 3;
case warning = 4;
case info = 5;
case light = 6;
case dark = 7;
/**
* Optionnal labels definition
*/
private const CLASSES = [
self::primary->name => "primary",
self::secondary->name => "secondary",
self::success->name => "success",
self::danger->name => "danger",
self::warning->name => "warning",
self::info->name => "info",
self::light->name => "light",
self::dark->name => "dark",
];
/**
* Get Class
*
* @return string
*/
public function class(): string
{
return self::CLASSES[$this->name];
}
/**
* Get enum classes
*
* @return array<string>
*/
public static function toClasses(): array
{
return self::CLASSES;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums\Invoices;
use App\Traits\Enums\BaseEnum;
enum InvoiceStatusEnum: int
{
use BaseEnum;
case opened = 1;
case refused = 2;
case canceled = 3;
case archived = 4;
case awaiting_signature = 5;
case awaiting_invoicing = 6;
/**
* Optionnal labels definition.
*/
private const LABELS = [
self::opened->name => "Ouvert (nouveaux ou en cours)",
self::refused->name => "Refusé (par le client ou aucune réponse de sa part)",
self::canceled->name => "Annulé (supprimé)",
self::archived->name => "Archivé (validé et envoyé à la comptabilité)",
self::awaiting_signature->name => "En attente de signature",
self::awaiting_invoicing->name => "En attente de facturation",
];
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Enums\Invoices;
use App\Traits\Enums\BaseEnum;
enum TaskTypeEnum: int
{
use BaseEnum;
case web = 1;
case seo = 2;
case package = 3;
case arbitrary = 4;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::web->name => "Web",
self::seo->name => "SEO",
self::package->name => "Forfait",
self::arbitrary->name => "Libre",
];
/**
* Custom added prices definition
*/
private const PRICES = [
self::web->name => 600,
self::seo->name => 690,
// * Linked to packages table
self::package->name => 0,
// * Arbitrary number
self::arbitrary->name => 0,
];
/**
* Get Price
*
* @return string
*/
public function price(): string
{
return self::PRICES[$this->name];
}
/**
* Get enum prices
*
* @return array<string>
*/
public static function toPrices(): array
{
return self::PRICES;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentytwo = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
enum MessageTypeEnum: int
{
use BaseEnum;
case private = 1;
case public = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::private->name => "Privé",
self::public->name => "Public",
];
}

View file

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* Defines the available information types.
*/
enum NoticeTypeEnum: int
{
use BaseEnum;
case primary = 1;
case secondary = 2;
case success = 3;
case danger = 4;
case warning = 5;
case info = 6;
case light = 7;
case dark = 8;
/**
* Optionnal labels definition
*/
private const CLASSES = [
self::primary->name => 'bg-primary text-primary-foreground',
self::secondary->name => 'bg-secondary text-secondary-foreground',
self::success->name => 'bg-successive text-successive-foreground',
self::danger->name => 'bg-destructive text-destructive-foreground',
self::warning->name => 'bg-preventive text-preventive-foreground',
self::info->name => 'bg-informative text-informative-foreground',
self::light->name => 'bg-muted text-muted-foreground',
self::dark->name => 'bg-foreground text-background',
];
/**
* Get Class.
*/
public function class(): string
{
return self::CLASSES[$this->name];
}
/**
* Get enum classes.
*
* @return array<string>
*/
public static function toClasses(): array
{
return self::CLASSES;
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Enums\Tickets;
use App\Traits\Enums\BaseEnum;
enum TicketFileTypeEnum: int
{
use BaseEnum;
case png = '.png';
case jpeg = '.jpeg';
case jpg = '.jpg';
case pdf = '.pdf';
case pptx = '.pptx';
case docx = '.docx';
case xlsx = '.xlsx';
case csv = '.csv';
case svg = '.svg';
/**
* Optionnal labels definition
*/
private const LABELS = [
self::png->name => "Fichier png",
self::jpeg->name => "Fichier jpeg",
self::jpg->name => "Fichier jpg",
self::pdf->name => "Fichier pdf",
self::pptx->name => "Fichier pptx",
self::docx->name => "Fichier docx",
self::xlsx->name => "Fichier xlsx",
self::csv->name => "Fichier csv",
self::svg->name => "Fichier svg",
];
/**
* Custom added mimes type definition
*/
private const MIMETYPE = [
self::png->name => 'image/png',
self::jpeg->name => 'image/jpeg',
self::jpg->name => 'image/jpeg',
self::pdf->name => 'application/pdf',
self::pptx->name => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
self::docx->name => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
self::xlsx->name => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
self::csv->name => 'text/csv',
self::svg->name => 'image/svg+xml',
];
/**
* Get Class
*
* @return string
*/
public function mimetypes(): string
{
return self::MIMETYPE[$this->name];
}
/**
* Get enum classes
*
* @return array<string>
*/
public static function toMimetypes(): array
{
return self::MIMETYPE;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace App\Enums\Tickets;
use App\Traits\Enums\BaseEnum;
enum TicketStatusEnum: int
{
use BaseEnum;
case new = 1;
case estimateTodo = 2;
case takenInAccount = 3;
case waiting = 4;
case waitingValidation = 5;
case recette = 6;
case recetteValidated = 7;
case production = 8;
case archived = 9;
case inDev = 10;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::new->name => "Nouveau",
self::estimateTodo->name => "Étude chef(s) de projet",
self::takenInAccount->name => "À planifier",
self::waiting->name => "En attente de réponse",
self::waitingValidation->name => "En attente de validation",
self::recette->name => "En recettage",
self::recetteValidated->name => "Recettage validé",
self::production->name => "En production",
self::archived->name => "Archivé",
self::inDev->name => "En cours de développement",
];
/**
* Custom added classes definition
*/
private const CLASSES = [
self::new->name => 'text-danger',
self::estimateTodo->name => 'text-info',
self::takenInAccount->name => 'text-info',
self::waiting->name => 'text-white bg-danger',
self::waitingValidation->name => 'text-warning bg-danger',
self::recette->name => 'text-primary',
self::recetteValidated->name => 'text-success',
self::production->name => 'text-success',
self::archived->name => 'text-secondary',
self::inDev->name => 'text-discovery',
];
/**
* Get Class
*
* @return string
*/
public function class(): string
{
return self::CLASSES[$this->name];
}
/**
* Get enum classes
*
* @return array<string>
*/
public static function toClasses(): array
{
return self::CLASSES;
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Enums\Tickets;
use App\Traits\Enums\BaseEnum;
enum TicketTypeEnum: int
{
use BaseEnum;
case bug = 1;
case evolution = 2;
case br = 3;
case newsletter = 4;
case qrcode = 5;
case advice = 6;
case complaints = 7;
case mailing = 8;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::bug->name => "Bug",
self::evolution->name => "Évolution",
self::br->name => "Demande BR",
self::newsletter->name => "Demande newsletter",
self::qrcode->name => "Demande de QR Code",
self::advice->name => "Conseil - Accompagnement",
self::complaints->name => "Réclamations consommateurs",
self::mailing->name => "Demande mail en masse",
];
/**
* Custom added classes definition
*/
private const CLASSES = [
self::bug->name => "bg-danger",
self::evolution->name => "bg-success",
self::br->name => "bg-primary",
self::newsletter->name => "bg-warning",
self::qrcode->name => "bg-secondary",
self::advice->name => "bg-info",
self::complaints->name => "bg-dark",
self::mailing->name => "bg-discovery",
];
/**
* Get Class
*
* @return string
*/
public function class(): string
{
return self::CLASSES[$this->name];
}
/**
* Get enum classes
*
* @return array<string>
*/
public static function toClasses(): array
{
return self::CLASSES;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
enum RoleEnum: int
{
use BaseEnum;
case developper = 0;
case admin = 1;
case client = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::developper->name => "Developpeur",
self::admin->name => "Administrateur",
self::client->name => "Client",
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Events;
use Domain\Models\ValueObjects\Email;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
readonly class BrevoWebhookCallReceived
{
use Dispatchable;
public function __construct(
public Email $from,
public Email $to,
public string $htmlRawContent
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Ticket;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NotifyAdmin
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Ticket */
public $ticket;
/** @var string */
public $corpsMaj;
/**
* Create a new event instance.
*
* @param \App\Models\Ticket $ticket
* @param string $corpsMaj
* @return void
*/
public function __construct(Ticket $ticket, string $corpsMaj)
{
$this->ticket = $ticket;
$this->corpsMaj = $corpsMaj;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Events;
use App\Models\Ticket;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class NotifySubscriptions
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Ticket */
public $ticket;
/** @var string */
public $corpsMaj;
/** @var Collection */
public $ignoreUsers;
/**
* Create a new event instance.
*
* @param \App\Models\Ticket $ticket
* @param string $corpsMaj
* @param \Illuminate\Support\Collection|null $ignoreUsers
* @return void
*/
public function __construct(Ticket $ticket, string $corpsMaj, Collection $ignoreUsers = null)
{
$this->ticket = $ticket;
$this->corpsMaj = $corpsMaj;
$this->ignoreUsers = $ignoreUsers;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Ticket;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NotifyTicketCreator
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Ticket */
public $ticket;
/** @var string */
public $corpsMaj;
/**
* Create a new event instance.
*
* @param \App\Models\Ticket $ticket
* @param string $corpsMaj
* @return void
*/
public function __construct(Ticket $ticket, string $corpsMaj)
{
$this->ticket = $ticket;
$this->corpsMaj = $corpsMaj;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Site;
use App\Models\Ticket;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketCreated implements ShouldQueue
{
use Dispatchable;
use SerializesModels;
public function __construct(
public readonly Site $siteReceivingTicket,
public readonly Ticket $ticket,
public readonly string $byUserName
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace App\Events;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketStatusChanged
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Site */
public $ticket;
/** @var \App\Models\User */
public $user;
/** @var \App\Enums\Tickets\TicketStatusEnum */
public $status;
/** @var string */
public $message;
/** @var boolean */
public $is_private;
/** @var boolean */
public $status_changed;
/**
* Create a new event instance.
*
* @param \App\Models\Ticket $ticket
* @param \App\Enums\Tickets\TicketStatusEnum $status
* @param \App\Models\User|null $user
* @param string|null $message
* @param boolean $is_private
* @param boolean $status_changed
* @return void
*/
public function __construct(
Ticket $ticket,
TicketStatusEnum $status,
User $user = null,
string $message = null,
bool $is_private = false,
bool $status_changed = true
) {
$this->ticket = $ticket;
$this->status = $status;
$this->user = $user;
$this->message = $message;
$this->is_private = $is_private;
$this->status_changed = $status_changed;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}

View file

@ -1,139 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Requests\Auth\ForgotPasswordRequest;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\ResetPasswordRequest;
use App\Mails\Auth\ResetPassword;
use App\Models\User;
use App\Models\UserPassword;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
/**
* Manages backend authentication sessions (login/logout).
*/
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function login(): Response
{
return Inertia::render('back/Pages/Auth/Login');
}
/**
* Handle an incoming authentication request.
*/
public function postLogin(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route(config('uwil-backend.routes.name') . 'dashboard.index', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function logout(Request $request): RedirectResponse
{
Auth::guard('backend')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route(config('uwil-backend.routes.name') . 'login');
}
/**
* Display the forgotten password view.
*/
public function forgotPassword(): Response
{
return Inertia::render('back/Pages/Auth/ForgotPassword');
}
/**
* Handle an incoming forgotten password request.
*/
public function postForgotPassword(ForgotPasswordRequest $request): RedirectResponse
{
$status = Password::broker('backend')->sendResetLink(
$request->validated()
);
if ($status !== Password::RESET_LINK_SENT) {
Log::warning('Password reset link not sent', [
'status' => $status,
'email' => $request->input('email'),
]);
}
return to_route(config('uwil-backend.routes.name') . 'login');
}
/**
* Send a reset password link to the user.
*/
public function sendResetPasswordLink(User $user): RedirectResponse
{
$status = Password::broker('backend')->sendResetLink(['email' => $user->email]);
return $status === Password::RESET_LINK_SENT
? back()
: back()->withErrors(['email' => trans('auth.throttled')]);
}
/**
* Display the view.
*/
public function resetPassword(string $token): Response
{
return Inertia::render('back/Pages/Auth/ResetPassword', [
'token' => $token,
'email' => request()->query('email', ''),
]);
}
/**
* Handle an incoming resetting password request.
*/
public function postResetPassword(ResetPasswordRequest $request): RedirectResponse
{
$status = Password::broker('backend')->reset(
$request->validated(),
function (User $user, string $password): void {
$user->setRememberToken(Str::random(60));
$user->save();
UserPassword::updateOrCreate(
['user_id' => $user->getKey()],
['password' => Hash::make($password)],
);
event(new PasswordReset($user));
Mail::send(new ResetPassword($user));
}
);
return $status === Password::PASSWORD_RESET
? to_route(config('uwil-backend.routes.name') . 'login')
: back()->withErrors(['email' => trans('auth.token')]);
}
}

View file

@ -1,250 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Brands\StoreBrandRequest;
use App\Http\Requests\Back\Brands\UpdateBrandRequest;
use App\Models\Brand;
use App\Models\Site;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for Brand in the backend.
*/
class BrandController extends BackendController
{
/**
* Gitlab projects collection.
*
* @var \Illuminate\Support\Collection<int,\App\Models\Site>
*/
private readonly Collection $siteModels;
/**
* Create the controller instance.
*/
public function __construct()
{
parent::__construct();
$this->authorizeResource(Brand::class);
$this->modelName = Brand::modelName();
$this->modelPage = Brand::modelPage();
$this->siteModels = Site::getModelsCached();
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Brand> $query */
$query = Brand::query();
if ($request->routeIs("{$this->routeName}{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Brand $brand): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($brand),
'siteModels' => $this->siteModels,
]);
}
/**
* Show a specific resource.
*/
public function show(Brand $brand): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($brand->load([
'sites' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreBrandRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$validatedData = $request->validated();
$brand = new Brand();
$brand->fill(Arr::except($validatedData, 'sites'));
if ($brand->saveOrFail()) {
$brand->sites()->syncWithoutDetaching(Arr::get($validatedData, 'sites'));
return to_route("{$this->routeName}{$this->modelName}.edit", $brand)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Brand $brand): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($brand->load(['sites'])),
'siteModels' => $this->siteModels,
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateBrandRequest $request,
Brand $brand,
): RedirectResponse {
return DB::transaction(function () use ($request, $brand) {
$validatedData = $request->validated();
$brand->fill(Arr::except($validatedData, 'sites'));
if ($brand->saveOrFail()) {
$brand->sites()->sync(Arr::get($validatedData, 'sites'));
return to_route("{$this->routeName}{$this->modelName}.edit", $brand)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return to_route("{$this->routeName}{$this->modelName}.edit", $brand)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(Brand $brand): Response
{
$brand->loadMissing(Brand::getCacheRelations());
$replica = $brand->replicate();
collect($brand->getRelations())->each(fn ($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**
* Restore the specified resource.
*/
public function restore(int $brand): RedirectResponse
{
$brand = Brand::withTrashed()->findOrFail($brand);
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($brand->restore()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(Brand $brand): RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($brand->delete()) {
return to_route("{$this->routeName}{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(Brand | int $brand): RedirectResponse
{
if (is_int($brand)) {
$brand = Brand::withTrashed()->findOrFail($brand);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($brand->forceDelete()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -1,229 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\InvoiceCompanies\StoreInvoiceCompanyRequest;
use App\Http\Requests\Back\InvoiceCompanies\UpdateInvoiceCompanyRequest;
use App\Models\InvoiceCompany;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for InvoiceCompany in the backend.
*/
class InvoiceCompanyController extends BackendController
{
/**
* Create the controller instance.
*/
public function __construct()
{
parent::__construct();
$this->authorizeResource(InvoiceCompany::class);
$this->modelName = InvoiceCompany::modelName();
$this->modelPage = InvoiceCompany::modelPage();
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\InvoiceCompany> $query */
$query = InvoiceCompany::query();
if ($request->routeIs("{$this->routeName}{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(InvoiceCompany $invoice_company): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($invoice_company),
]);
}
/**
* Show a specific resource.
*/
public function show(InvoiceCompany $invoice_company): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($invoice_company),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreInvoiceCompanyRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$invoice_company = new InvoiceCompany();
$invoice_company->fill($request->validated());
if ($invoice_company->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_company)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(InvoiceCompany $invoice_company): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($invoice_company),
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateInvoiceCompanyRequest $request,
InvoiceCompany $invoice_company
): RedirectResponse {
return DB::transaction(function () use ($request, $invoice_company) {
$invoice_company->fill($request->validated());
if ($invoice_company->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_company)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_company)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(InvoiceCompany $invoice_company): Response
{
$invoice_company->loadMissing(InvoiceCompany::getCacheRelations());
$replica = $invoice_company->replicate();
collect($invoice_company->getRelations())
->each(fn ($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**
* Restore the specified resource.
*/
public function restore(int $invoice_company): RedirectResponse
{
$invoice_company = InvoiceCompany::withTrashed()->findOrFail($invoice_company);
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_company->restore()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(InvoiceCompany $invoice_company): RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_company->delete()) {
return to_route("{$this->routeName}{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(InvoiceCompany | int $invoice_company): RedirectResponse
{
if (is_int($invoice_company)) {
$invoice_company = InvoiceCompany::withTrashed()->findOrFail($invoice_company);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_company->forceDelete()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -1,229 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\InvoicePackages\StoreInvoicePackageRequest;
use App\Http\Requests\Back\InvoicePackages\UpdateInvoicePackageRequest;
use App\Models\InvoicePackage;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for InvoicePackage in the backend.
*/
class InvoicePackageController extends BackendController
{
/**
* Create the controller instance.
*/
public function __construct()
{
parent::__construct();
$this->authorizeResource(InvoicePackage::class);
$this->modelName = InvoicePackage::modelName();
$this->modelPage = InvoicePackage::modelPage();
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\InvoicePackage> $query */
$query = InvoicePackage::query();
if ($request->routeIs("{$this->routeName}{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(InvoicePackage $invoice_package): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($invoice_package),
]);
}
/**
* Show a specific resource.
*/
public function show(InvoicePackage $invoice_package): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($invoice_package),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreInvoicePackageRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$invoice_package = new InvoicePackage();
$invoice_package->fill($request->validated());
if ($invoice_package->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_package)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(InvoicePackage $invoice_package): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($invoice_package),
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateInvoicePackageRequest $request,
InvoicePackage $invoice_package
): RedirectResponse {
return DB::transaction(function () use ($request, $invoice_package) {
$invoice_package->fill($request->validated());
if ($invoice_package->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_package)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return to_route("{$this->routeName}{$this->modelName}.edit", $invoice_package)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(InvoicePackage $invoice_package): Response
{
$invoice_package->loadMissing(InvoicePackage::getCacheRelations());
$replica = $invoice_package->replicate();
collect($invoice_package->getRelations())
->each(fn ($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**
* Restore the specified resource.
*/
public function restore(int $invoice_package): RedirectResponse
{
$invoice_package = InvoicePackage::withTrashed()->findOrFail($invoice_package);
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_package->restore()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(InvoicePackage $invoice_package): RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_package->delete()) {
return to_route("{$this->routeName}{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(InvoicePackage | int $invoice_package): RedirectResponse
{
if (is_int($invoice_package)) {
$invoice_package = InvoicePackage::withTrashed()->findOrFail($invoice_package);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($invoice_package->forceDelete()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -1,228 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Notices\StoreNoticeRequest;
use App\Http\Requests\Back\Notices\UpdateNoticeRequest;
use App\Models\Notice;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for Notice in the backend.
*/
class NoticeController extends BackendController
{
/**
* Create the controller instance.
*/
public function __construct()
{
parent::__construct();
$this->authorizeResource(Notice::class);
$this->modelName = Notice::modelName();
$this->modelPage = Notice::modelPage();
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Notice> $query */
$query = Notice::query();
if ($request->routeIs("{$this->routeName}{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'title',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.title'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Notice $notice): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($notice),
]);
}
/**
* Show a specific resource.
*/
public function show(Notice $notice): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($notice),
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreNoticeRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$notice = new Notice();
$notice->fill($request->validated());
if ($notice->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $notice)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Notice $notice): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($notice),
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateNoticeRequest $request,
Notice $notice
): RedirectResponse {
return DB::transaction(function () use ($request, $notice) {
$notice->fill($request->validated());
if ($notice->saveOrFail()) {
return to_route("{$this->routeName}{$this->modelName}.edit", $notice)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return to_route("{$this->routeName}{$this->modelName}.edit", $notice)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(Notice $notice): Response
{
$notice->loadMissing(Notice::getCacheRelations());
$replica = $notice->replicate();
collect($notice->getRelations())->each(fn ($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**
* Restore the specified resource.
*/
public function restore(int $notice): RedirectResponse
{
$notice = Notice::withTrashed()->findOrFail($notice);
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($notice->restore()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(Notice $notice): RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($notice->delete()) {
return to_route("{$this->routeName}{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(Notice | int $notice): RedirectResponse
{
if (is_int($notice)) {
$notice = Notice::withTrashed()->findOrFail($notice);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($notice->forceDelete()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -1,287 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Sites\StoreSiteRequest;
use App\Http\Requests\Back\Sites\UpdateSiteRequest;
use App\Models\Site;
use App\Models\User;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Response;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
use Uwil\LaravelBackend\Support\UserRoleRegistry;
/**
* Handles CRUD operations for Site in the backend.
*/
class SiteController extends BackendController
{
/**
* Project managers collection.
*
* @var \Illuminate\Support\Collection<int,\App\Models\User>
*/
private readonly Collection $userModelsProjectManager;
/**
* Project managers collection.
*
* @var \Illuminate\Support\Collection<int,\App\Models\User>
*/
private readonly Collection $userModelsClient;
/**
* Create the controller instance.
*/
public function __construct()
{
parent::__construct();
$this->authorizeResource(Site::class);
$this->modelName = Site::modelName();
$this->modelPage = Site::modelPage();
/** @var \Illuminate\Support\Collection<int, \App\Models\User> $userModels */
$userModels = User::getModelsCached()
->where('published', true)
->where('deleted_at', null)
->values();
$this->userModelsProjectManager = $userModels
->filter(fn (User $user) => $user->role->value === UserRoleRegistry::get('project-manager')->value)
->values();
$this->userModelsClient = $userModels
->filter(fn (User $user) => $user->role->value === UserRoleRegistry::get('client')->value)
->values();
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Site> $query */
$query = Site::query()
->when(
$userModel->role->value === UserRoleRegistry::get('client')->value,
fn ($query) => $query->whereIn('id', $userModel->sites->modelKeys()),
)
->with(['projectManager', 'gitlabProject']);
if ($request->routeIs("{$this->routeName}{$this->modelName}.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(Site $site): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($site),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
]);
}
/**
* Show a specific resource.
*/
public function show(Site $site): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($site->load([
'projectManager' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'gitlabProject' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'users' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
]);
}
/**
* Store a newly created resource.
*/
public function store(StoreSiteRequest $request): RedirectResponse
{
return DB::transaction(function () use ($request) {
$validatedData = $request->validated();
$site = new Site();
$site->fill(Arr::except($validatedData, 'users'));
if ($site->saveOrFail()) {
$site->users()->syncWithoutDetaching(Arr::get($validatedData, 'users'));
return to_route("{$this->routeName}{$this->modelName}.edit", $site)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Site $site): Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($site->load([
'projectManager' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'gitlabProject' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'users' => fn ($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
]);
}
/**
* Update the specified resource.
*/
public function update(
UpdateSiteRequest $request,
Site $site
): RedirectResponse {
return DB::transaction(function () use ($request, $site) {
$validatedData = $request->validated();
$site->fill(Arr::except($validatedData, 'users'));
if ($site->saveOrFail()) {
$site->users()->sync(Arr::get($validatedData, 'users'));
return to_route("{$this->routeName}{$this->modelName}.edit", $site)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return to_route("{$this->routeName}{$this->modelName}.edit", $site)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*/
public function duplicate(Site $site): Response
{
$site->loadMissing(Site::getCacheRelations());
$replica = $site->replicate();
collect($site->getRelations())->each(fn ($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**
* Restore the specified resource.
*/
public function restore(int $site): RedirectResponse
{
$site = Site::withTrashed()->findOrFail($site);
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($site->restore()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function destroy(Site $site): RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($site->delete()) {
return to_route("{$this->routeName}{$this->modelName}.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*/
public function forceDestroy(Site | int $site): RedirectResponse
{
if (is_int($site)) {
$site = Site::withTrashed()->findOrFail($site);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str((string) \parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($site->forceDelete()) {
return to_route("{$this->routeName}{$this->modelName}.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Back;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Uwil\LaravelBackend\Controllers\UserController as BackendUserController;
/**
* Handles CRUD operations for Site in the backend.
*/
class UserController extends BackendUserController
{
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
/**
* Update the specified resource.
*/
#[\Override]
public function store(
Request $request,
mixed $callback = null,
): RedirectResponse {
return parent::store($request, function ($request, $user): void {
$validatedData = $request->validated();
$user->sites()->syncWithoutDetaching(Arr::get($validatedData, 'sites'));
if (Arr::get($validatedData, 'gitlab_id')) {
$user->gitlabUser()->create([
'gitlab_id' => Arr::get($validatedData, 'gitlab_id'),
]);
}
});
}
/**
* Update the specified resource.
*/
#[\Override]
public function update(
Request $request,
mixed $user,
mixed $callback = null,
): RedirectResponse {
return parent::update($request, $user, function ($request, $user): void {
$validatedData = $request->validated();
$user->sites()->sync(Arr::get($validatedData, 'sites'));
if (Arr::get($validatedData, 'gitlab_id')) {
$user->gitlabUser()->updateOrCreate(
['user_id' => $user->id],
['gitlab_id' => $validatedData['gitlab_id']],
);
} else {
$user->gitlabUser()->delete();
}
});
}
// phpcs:enable
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ConfirmsPasswords;
class ConfirmPasswordController extends Controller
{
use ConfirmsPasswords;
/**
* Where to redirect users when the intended url fails.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
/**
* ForgotPasswordController
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend');
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend')->except('logout');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Validation\Rules\Password;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Get the password reset validation rules.
*
* @return array
*/
protected function rules()
{
return [
'token' => 'required',
'email' => 'required|email',
'password' => [
'required', 'confirmed', Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised()
],
];
}
}

View file

@ -0,0 +1,186 @@
<?php
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\Models\Brand;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class BrandController extends Controller
{
/**
* 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 $brandModels */
$brandModels = Brand::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$brandModels,
$search,
null,
'name',
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($brandModels);
/** Custom pagination */
$brandModels = $this->customPaginate($brandModels, $request->pagination);
return view('bo.pages.brands.index', compact('brandModels', 'search', 'searchFields'));
}
/**
* Show model action.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function show(Brand $brand): \Illuminate\Contracts\View\View
{
return view('bo.pages.brands.show', ['brandModel' => $brand]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brand): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.brands.create', [
'brandModel' => $brand,
'siteModels' => $siteModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Brands\StoreBrandRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreBrandRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$brand = new Brand();
$brand->fill($request->safe()->except('sites'));
if ($brand->saveOrFail()) {
$brand->updateSites($brand);
return redirect()
->route('bo.brands.edit', $brand)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.brands'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.brands'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function edit(Brand $brand): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.brands.edit', [
'brandModel' => $brand,
'siteModels' => $siteModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Brands\UpdateBrandRequest $request
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateBrandRequest $request, Brand $brand): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $brand) {
$brand->fill($request->safe()->except(['sites']));
$brand->updateSites($brand);
if ($brand->saveOrFail()) {
return redirect()
->route('bo.brands.edit', $brand)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.brands'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.brands'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Brand $brand): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($brand) {
if (count($brand->accounts)) {
return redirect()
->route('bo.brands.index')
->with('error', __(
':model n\'as pas pu être supprimé car elle est encore associée avec des Sites.',
['model' => Str::singular(trans('models.classes.brands'))]
));
} else {
$brand->deleteSites($brand);
if ($brand->deleteOrFail()) {
return redirect()
->route('bo.brands.index')
->with('success', __(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.brands'))]
));
}
return redirect()
->back()
->with('error', __(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.brands'))]
));
} //end if
});
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Events\BrevoWebhookCallReceived;
use App\Http\Requests\Bo\BrevoWebhookRequest;
use Domain\Models\ValueObjects\Email;
use Psr\Log\LoggerInterface;
use Illuminate\Routing\Controller as BaseController;
use \Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class BrevoWebhookController extends BaseController
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly Dispatcher $dispatcher
) {}
public function webhookCallReceived(BrevoWebhookRequest $request): JsonResponse
{
$fromEmail = $request->getFromAddress();
$toEmail = $request->getToAddress();
$formContent = $request->getRawHtmlBody();
if (!$this->isFromLdc($fromEmail)) {
$this->logger->warning(
'Received a brevo webhook call from a non ldc email.',
[
'from' => $fromEmail,
'to' => $toEmail,
'content' => base64_encode($formContent)
]
);
return response()->json(
['ok' => false],
Response::HTTP_BAD_REQUEST
);
}
$this->dispatcher->dispatch(
new BrevoWebhookCallReceived(
from: $fromEmail,
to: $toEmail,
htmlRawContent: $formContent,
)
);
return response()->json(['ok' => true]);
}
private function isFromLdc(Email $email): bool
{
return substr(strrchr($email->value, "@"), 1) === 'ldc.fr';
}
}

View file

@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Companies\StoreCompanyRequest;
use App\Http\Requests\Bo\Companies\UpdateCompanyRequest;
use App\Models\Company;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CompanyController extends Controller
{
/**
* 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 $companyModels */
$companyModels = Company::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$companyModels,
$search,
null,
'name'
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($companyModels);
/** Custom pagination */
$companyModels = $this->customPaginate($companyModels, $request->pagination);
return view('bo.pages.companies.index', compact('companyModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Company $company
* @return \Illuminate\Contracts\View\View
*/
public function create(Company $company): \Illuminate\Contracts\View\View
{
return view('bo.pages.companies.create', [
'companyModel' => $company,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Companies\StoreCompanyRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreCompanyRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$company = new Company();
$company->fill($request->validated());
if ($company->saveOrFail()) {
return redirect()
->route('bo.companies.edit', $company)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.companies'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.companies'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Company $company
* @return \Illuminate\Contracts\View\View
*/
public function edit(Company $company): \Illuminate\Contracts\View\View
{
return view('bo.pages.companies.edit', [
'companyModel' => $company
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Companies\UpdateCompanyRequest $request
* @param \App\Models\Company $company
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateCompanyRequest $request, Company $company): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $company) {
$company->fill($request->validated());
if ($company->saveOrFail()) {
return redirect()
->route('bo.companies.edit', $company)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.companies'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.companies'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Company $company
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Company $company): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($company) {
if (count($company->invoices()->get()->toArray())) {
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_deleted_relationship',
[
'model' => Str::singular(trans('models.classes.companies')),
'relationship' => trans('models.classes.invoices')
]
));
}
if ($company->deleteOrFail()) {
return redirect()
->route('bo.companies.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.companies'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.companies'))]
));
});
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Bo\Credit;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\CreditAccounts\StoreCreditAccountRequest;
use App\Models\Brand;
use App\Models\Credit\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AccountController extends Controller
{
/**
* 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 $user */
$user = auth()->user();
if (!$user->isAdmin()) {
$brandModels = $user->brands()->pluck('id');
} else {
$brandModels = $request->brand ?? Brand::all()->pluck('id');
}
/** @var \Illuminate\Database\Eloquent\Builder $credit_accountModels */
$credit_accountModels = Account::query()->whereIn('brand_id', $brandModels);
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$credit_accountModels,
$search,
null,
'label',
'balance',
);
}
$searchFields = trans('Label, solde');
/** Sort columns with a query */
$this->sortQuery($credit_accountModels);
/** Custom pagination */
$credit_accountModels = $this->customPaginate($credit_accountModels, $request->pagination);
return view('bo.pages.credit_accounts.index', compact(
'credit_accountModels',
'brandModels',
'search',
'searchFields'
));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brand, Account $account): \Illuminate\Contracts\View\View
{
return view('bo.pages.credit_accounts.create', [
'brandModel' => $brand,
'credit_accountModel' => $account
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Models\Brand $brand
* @param \App\Http\Requests\Bo\CreditAccounts\StoreCreditAccountRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Brand $brand, StoreCreditAccountRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($brand, $request) {
$credit_accountModel = new Account();
$credit_accountModel->brand_id = $brand->id;
$credit_accountModel->fill($request->validated());
if ($credit_accountModel->saveOrFail()) {
return redirect()
->route('bo.brands.credit_accounts.index', $brand)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.accounts'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.accounts'))]
));
});
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Bo\Credit;
use App\Enums\Credit\BalanceOperationEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\CreditLogs\StoreCreditLogRequest;
use App\Models\Brand;
use App\Models\Credit\Account;
use App\Models\Credit\CreditLog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CreditLogController extends Controller
{
/**
* 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
{
$brandModel = $request->brand;
$credit_accountModel = $request->account;
/** @var \Illuminate\Database\Eloquent\Builder $credit_logModels */
$credit_logModels = CreditLog::query()->where('credit_account_id', $credit_accountModel->id);
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$credit_logModels,
$search,
null,
'amount',
);
}
$searchFields = trans('Montant');
/** Sort columns with a query */
$this->sortQuery($credit_logModels);
/** Custom pagination */
$credit_logModels = $this->customPaginate($credit_logModels, $request->pagination);
return view('bo.pages.credit_logs.index', compact(
'credit_logModels',
'credit_accountModel',
'brandModel',
'search',
'searchFields'
));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brand, Account $account): \Illuminate\Contracts\View\View
{
return view('bo.pages.credit_logs.create', [
'brandModel' => $brand,
'credit_accountModel' => $account
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @param \App\Http\Requests\Bo\CreditLogs\StoreCreditLogRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(
Brand $brand,
Account $account,
StoreCreditLogRequest $request
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($brand, $account, $request) {
$credit_logModel = new CreditLog();
$credit_logModel->credit_account_id = $request->account->id;
$credit_logModel->type = BalanceOperationEnum::credit;
$credit_logModel->fill($request->validated());
if ($credit_logModel->saveOrFail()) {
return redirect()
->route('bo.brands.credit_accounts.index', ['brand' => $brand, 'account' => $account])
->with('success', trans(
':model a été crédité',
['model' => Str::singular(trans('models.classes.accounts'))]
));
}
return redirect()
->back()
->with('error', trans(
':model ne peut être crédité',
['model' => Str::singular(trans('models.classes.accounts'))]
));
});
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Bo\Credit;
use App\Enums\Credit\EstimationStatusEnum;
use App\Enums\Tickets\TicketStatusEnum;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Credit\AnswerEstimateRequest;
use App\Http\Requests\Bo\Credit\CreateEstimateRequest;
use App\Models\Credit\Estimation;
use App\Models\Ticket;
use Illuminate\Support\Facades\DB;
class EstimationController extends Controller
{
/**
* Show estimate Form
*
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function estimate(Ticket $ticket)
{
$this->authorize('estimate', $ticket);
return view('estimations.create', \compact('ticket'));
}
/**
* Create Ticket estimation
*
* @param \App\Http\Requests\Bo\Credit\CreateEstimateRequest $request
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function estimateSubmit(CreateEstimateRequest $request, Ticket $ticket)
{
return DB::transaction(function () use ($request, $ticket) {
/** @var \App\Models\User */
$user = auth()->user();
$estimate = new Estimation($request->validated());
$estimate->ticket()->associate($ticket);
$estimate->estimator()->associate($user);
$ticket->status = TicketStatusEnum::waitingValidation;
$ticket->saveQuietly();
if ($estimate->saveOrFail()) {
$message = sprintf(
'<b>%s</b> a modifié le statut en <b>%s</b>, Une estimation de %s jours à été ajoutée',
$user->name,
$ticket->status->label(),
\number_format(\floatval($request->amount), 1, ',', '')
);
event(new TicketStatusChanged($ticket, $ticket->status, $user));
event(new NotifySubscriptions($ticket, $message));
event(new NotifyAdmin($ticket, $message));
return response()->redirectToRoute('bo.ticket.show', $ticket)
->with('info', trans('L\'estimation à bien été ajouté'));
}
return \back();
});
}
/**
* Answer ticket estimation
*
* @param \App\Http\Requests\Bo\Credit\AnswerEstimateRequest $request
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function estimatePendingSubmit(AnswerEstimateRequest $request, Ticket $ticket)
{
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\Credit\Estimation */
$estimation = $ticket->estimation;
if ($request->answer) {
$ticket->status = TicketStatusEnum::takenInAccount;
$estimation->status = EstimationStatusEnum::accepted;
} else {
$ticket->status = TicketStatusEnum::archived;
$estimation->status = EstimationStatusEnum::refused;
}
$estimation->validator()->associate($user);
if ($estimation->saveOrFail() and $ticket->saveOrFail()) {
$message = sprintf(
'Une réponse à été fourni par %s pour l\'estimation du ticket n°%d : %s',
$user->name,
$ticket->id,
$estimation->status->label()
);
event(new NotifySubscriptions($ticket, $message));
event(new NotifyAdmin($ticket, $message));
return response()->redirectToRoute('bo.ticket.show', $ticket)
->with('info', trans('Votre réponse à été enregistrée'));
}
return \back();
}
/**
* Handle submit and cancel an estimate on ticket
*
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function estimateCancelSubmit(Ticket $ticket)
{
$this->authorize('cancelEstimate', $ticket);
/** @var \App\Models\Credit\Estimation */
$estimation = $ticket->estimation;
$ticket->status = TicketStatusEnum::estimateTodo;
if ($estimation->deleteOrFail() and $ticket->saveOrFail()) {
return response()->redirectToRoute('bo.ticket.show', $ticket)
->with('info', trans('L\'estimation a été supprimée.'));
}
return \back();
}
}

View file

@ -0,0 +1,158 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Informations\StoreInformationRequest;
use App\Http\Requests\Bo\Informations\UpdateInformationRequest;
use App\Models\Information;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class InformationController extends Controller
{
/**
* 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 $informationModels */
$informationModels = Information::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$informationModels,
$search,
null,
'title',
'message'
);
}
$searchFields = trans('Titre, message');
/** Sort columns with a query */
$this->sortQuery($informationModels);
/** Custom pagination */
$informationModels = $this->customPaginate($informationModels, $request->pagination);
return view('bo.pages.informations.index', compact('informationModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Information $information
* @return \Illuminate\Contracts\View\View
*/
public function create(Information $information): \Illuminate\Contracts\View\View
{
return view('bo.pages.informations.create', [
'informationModel' => $information
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Informations\StoreInformationRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreInformationRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$information = new Information();
$information->fill($request->validated());
if ($information->saveOrFail()) {
return redirect()
->route('bo.informations.edit', $information)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.informations'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.informations'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Information $information
* @return \Illuminate\Contracts\View\View
*/
public function edit(Information $information): \Illuminate\Contracts\View\View
{
return view('bo.pages.informations.edit', [
'informationModel' => $information,
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Informations\UpdateInformationRequest $request
* @param \App\Models\Information $information
* @return \Illuminate\Http\RedirectResponse
* @ignore phpcs:disable Generic.Files.LineLength.TooLong
*/
public function update(UpdateInformationRequest $request, Information $information): \Illuminate\Http\RedirectResponse
{
// phpcs:enable
return DB::transaction(function () use ($request, $information) {
$information->fill($request->validated());
if ($information->saveOrFail()) {
return redirect()
->route('bo.informations.edit', $information)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.informations'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.informations'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Information $information
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Information $information): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($information) {
if ($information->deleteOrFail()) {
return redirect()
->route('bo.informations.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.informations'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.informations'))]
));
});
}
}

View file

@ -0,0 +1,227 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Invoices\InvoiceStatusEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Invoices\StoreInvoiceRequest;
use App\Http\Requests\Bo\Invoices\UpdateInvoiceRequest;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Package;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class InvoiceController extends Controller
{
/**
* 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
{
// If no sorting is requested, apply the default orderByDesc.
/** @var \Illuminate\Database\Eloquent\Builder $invoiceModels */
$invoiceModels = (!request()->query('sort_col') && !request()->query('sort_way'))
? Invoice::query()->orderByDesc('created_at')
: Invoice::query();
$status = intval($request->status);
switch ($status) {
case InvoiceStatusEnum::awaiting_signature->value:
$invoiceModels->whereNotNull('sent_client_date')->whereNull('signed_date');
break;
case InvoiceStatusEnum::awaiting_invoicing->value:
$invoiceModels->whereNotNull('sent_client_date')->whereNotNull('signed_date')->whereNull('sent_accounting_date');
break;
default:
if ($status !== 0) {
$invoiceModels->where('status', $status);
}
break;
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$invoiceModels,
$search,
null,
'firstname',
'lastname',
'ref',
'site_id',
);
}
$searchFields = trans('Nom, Prénom, Référence, Site');
/** Sort columns with a query */
$this->sortQuery($invoiceModels);
/** Custom pagination */
$invoiceModels = $this->customPaginate($invoiceModels, $request->pagination);
return view('bo.pages.invoices.index', compact('invoiceModels', 'search', 'searchFields', 'status'));
}
/**
* Show model action.
*
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Contracts\View\View
*/
public function show(Invoice $invoice): \Illuminate\Contracts\View\View
{
return view('bo.pages.invoices.show', ['invoiceModel' => $invoice]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Contracts\View\View
*/
public function create(Invoice $invoice): \Illuminate\Contracts\View\View
{
$packageModels = Package::where('published', true)->get();
$taskRowModel = Invoice::getTaskStructure();
$siteModels = Site::orderBy('name', 'ASC')->get();
$companyModels = Company::get();
return view('bo.pages.invoices.create', [
'invoiceModel' => $invoice,
'taskRowModel' => $taskRowModel,
'packageModels' => $packageModels,
'siteModels' => $siteModels,
'companyModels' => $companyModels,
'invoiceStatusArray' => $invoice->getInvoiceStatusArray($invoice),
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Invoices\StoreInvoiceRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreInvoiceRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$invoice = new Invoice();
$invoice->fill($request->validated());
if ($invoice->saveOrFail()) {
return redirect()
->route('bo.invoices.edit', $invoice)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.invoices'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.invoices'))]
));
});
}
/**
* Edit model action.
*
* @param Invoice $invoice
* @return \Illuminate\Contracts\View\View
*/
public function edit(Invoice $invoice): \Illuminate\Contracts\View\View
{
$packageModels = Package::get();
$taskRowModel = Invoice::getTaskStructure();
$siteModels = Site::orderBy('name', 'ASC')->get();
$companyModels = Company::get();
return view('bo.pages.invoices.edit', [
'invoiceModel' => $invoice,
'taskRowModel' => $taskRowModel,
'packageModels' => $packageModels,
'siteModels' => $siteModels,
'companyModels' => $companyModels,
'invoiceStatusArray' => $invoice->getInvoiceStatusArray($invoice),
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Invoices\UpdateInvoiceRequest $request
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateInvoiceRequest $request, Invoice $invoice): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $invoice) {
$invoice->fill(($invoice->status->value === InvoiceStatusEnum::opened->value) ? $request->validated() : []);
if ($invoice->saveOrFail()) {
return redirect()
->route('bo.invoices.edit', $invoice)
->with('success', __(
'crud.messages.has_been_updated',
['model' => __('models.classes.invoices')]
));
} //end if
return redirect()
->back()
->with('error', __(
'crud.messages.cannot_be_updated',
['model' => __('models.classes.invoices')]
));
});
}
/**
* Download Invoice.
*
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function download(Invoice $invoice)
{
if (Gate::allows('isAdmin')) {
return $invoice->generateInvoice()
->stream(Str::slug(trans('Devis :appname :date - ref :ref', [
'appname' => config('app.name'),
'date' =>
$invoice->created_at !== $invoice->updated_at ?
$invoice->updated_at->isoFormat('YYYY-MM-DD') :
$invoice->created_at->isoFormat('YYYY-MM-DD'),
'ref' => $invoice->ref,
])) . '.pdf');
}
return redirect()->route('bo.invoices.index')
->with('error', __('crud.messages.cannot_be_updated', [
'model' => __('models.classes.invoices')
]));
}
/**
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{
$model = $request->targetModel;
return DB::transaction(function () use ($model) {
$models = $model::query()->orderBy('name', 'ASC')->paginate(12);
return \response()->json($models ?? 500);
});
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Labels\StoreLabelRequest;
use App\Http\Requests\Bo\Labels\UpdateLabelRequest;
use App\Models\Label;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class LabelController extends Controller
{
/**
* 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 $labelModels */
$labelModels = Label::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$labelModels,
$search,
null,
'name'
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($labelModels);
/** Custom pagination */
$labelModels = $this->customPaginate($labelModels, $request->pagination);
return view('bo.pages.labels.index', compact('labelModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Label $label
* @return \Illuminate\Contracts\View\View
*/
public function create(Label $label): \Illuminate\Contracts\View\View
{
return view('bo.pages.labels.create', [
'labelModel' => $label
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Labels\StoreLabelRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreLabelRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$label = new Label();
$label->fill($request->validated());
if ($label->saveOrFail()) {
return redirect()
->route('bo.labels.edit', $label)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.labels'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.labels'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Label $label
* @return \Illuminate\Contracts\View\View
*/
public function edit(Label $label): \Illuminate\Contracts\View\View
{
return view('bo.pages.labels.edit', [
'labelModel' => $label,
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Labels\UpdateLabelRequest $request
* @param \App\Models\Label $label
* @return \Illuminate\Http\RedirectResponse
* @ignore phpcs:disable Generic.Files.LineLength.TooLong
*/
public function update(UpdateLabelRequest $request, Label $label): \Illuminate\Http\RedirectResponse
{
// phpcs:enable
return DB::transaction(function () use ($request, $label) {
$label->fill($request->validated());
if ($label->saveOrFail()) {
return redirect()
->route('bo.labels.edit', $label)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.labels'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.labels'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Label $label
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Label $label): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($label) {
if ($label->deleteOrFail()) {
return redirect()
->route('bo.labels.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.labels'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.labels'))]
));
});
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Packages\StorePackageRequest;
use App\Http\Requests\Bo\Packages\UpdatePackageRequest;
use App\Models\Package;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class PackageController extends Controller
{
/**
* 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 $packageModels */
$packageModels = Package::query();
$status = $request->status;
if ($status === null) {
$packageModels->where('published', true);
} elseif ($status === '0') {
$packageModels->where('published', false);
} elseif ($status === '1') {
$packageModels->where('published', true);
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$packageModels,
$search,
null,
'name',
'amount'
);
}
$searchFields = trans('Nom, Montant');
/** Sort columns with a query */
$this->sortQuery($packageModels);
/** Custom pagination */
$packageModels = $this->customPaginate($packageModels, $request->pagination);
return view('bo.pages.packages.index', compact('packageModels', 'search', 'searchFields', 'status'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Package $package
* @return \Illuminate\Contracts\View\View
*/
public function create(Package $package): \Illuminate\Contracts\View\View
{
return view('bo.pages.packages.create', [
'packageModel' => $package,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Packages\StorePackageRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StorePackageRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$package = new Package();
$package->fill($request->validated());
if ($package->saveOrFail()) {
return redirect()
->route('bo.packages.edit', $package)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.packages'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.packages'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Package $package
* @return \Illuminate\Contracts\View\View
*/
public function edit(Package $package): \Illuminate\Contracts\View\View
{
return view('bo.pages.packages.edit', [
'packageModel' => $package,
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Packages\UpdatePackageRequest $request
* @param \App\Models\Package $package
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdatePackageRequest $request, Package $package): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $package) {
$package->fill($request->validated());
if ($package->saveOrFail()) {
return redirect()
->route('bo.packages.edit', $package)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.packages'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.packages'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Package $package
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Package $package): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($package) {
if ($package->deleteOrFail()) {
return redirect()
->route('bo.packages.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.packages'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.packages'))]
));
});
}
/**
* Change model published.
*
* @param \App\Models\Package $package
* @return \Illuminate\Http\RedirectResponse
*/
public function changePublished(Package $package)
{
$package->update(['published' => !$package->getOriginal('published')]);
return redirect()->back()->with('success', trans(__('crud.messages.publish_status_saved')));
}
}

View file

@ -0,0 +1,204 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Sites\StoreSiteRequest;
use App\Http\Requests\Bo\Sites\UpdateSiteRequest;
use App\Lib\Helpers\ToolboxHelper;
use App\Models\Site;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class SiteController extends Controller
{
/**
* 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 $user */
$user = Auth::user();
if ($user->isAdmin()) {
/** @var \Illuminate\Database\Eloquent\Builder $siteModels */
$siteModels = Site::query()->with(['invoices', 'tickets']);
} else {
/** @var \Illuminate\Database\Eloquent\Builder $siteModels */
$siteModels = Site::whereHas('users', function (\Illuminate\Database\Eloquent\Builder $query) use ($user) {
$query->where('id', $user->id)->with(['invoices', 'tickets']);
});
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$siteModels,
$search,
null,
'name',
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($siteModels);
/** Custom pagination */
$siteModels = $this->customPaginate($siteModels, $request->pagination);
return view('bo.pages.sites.index', compact('siteModels', 'search', 'searchFields'));
}
/**
* Show model action.
*
* @param \App\Models\Site $site
* @return \Illuminate\Contracts\View\View
*/
public function show(Site $site): \Illuminate\Contracts\View\View
{
return view('bo.pages.sites.show', ['siteModel' => $site]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Site $site
* @return \Illuminate\Contracts\View\View
*/
public function create(Site $site): \Illuminate\Contracts\View\View
{
$userModels = User::orderBy('name', 'ASC')->get();
return view('bo.pages.sites.create', [
'siteModel' => $site,
'userModels' => $userModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Sites\StoreSiteRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreSiteRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$site = new Site();
$site->fill($request->validated());
if ($site->saveOrFail()) {
$site->users()->attach(collect($request->validated('users'))->pluck('id'));
return redirect()
->route('bo.sites.edit', $site)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.sites'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.sites'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Site $site
* @return \Illuminate\Contracts\View\View
*/
public function edit(Site $site): \Illuminate\Contracts\View\View
{
$userModels = User::orderBy('name', 'ASC')->get();
return view('bo.pages.sites.edit', [
'siteModel' => $site,
'userModels' => $userModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Sites\UpdateSiteRequest $request
* @param \App\Models\Site $site
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateSiteRequest $request, Site $site): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $site) {
$site->fill($request->validated());
$site->users()->sync(collect($request->validated('users'))->pluck('id'));
if ($site->saveOrFail()) {
return redirect()
->route('bo.sites.edit', $site)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.sites'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.sites'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Site $site
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Site $site): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($site) {
if ($site->deleteOrFail()) {
$site->users()->detach();
return redirect()
->route('bo.sites.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.sites'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.sites'))]
));
});
}
/**
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request) {
$model = $request->targetModel;
$search = $request->search;
$models = $model::where("name", "LIKE", "%{$search}%")->orderBy("name", "ASC");
return ToolboxHelper::jsonPaginate($this->customPaginate($models, $request->pagination));
});
}
}

View file

@ -0,0 +1,933 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Models\Site;
use App\Models\User;
use App\Models\Ticket;
use App\Mail\NewTicket;
use App\Models\Message;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Jobs\UpdateIssueLabels;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Support\Facades\DB;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\LinkTicket;
use Illuminate\Support\Facades\Mail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Http\Requests\Bo\SortTickets;
use App\Http\Requests\Bo\StoreTicket;
use App\Http\Requests\Bo\UpdateTicket;
use App\Lib\Helpers\FileStorageHelper;
use App\Enums\Tickets\TicketStatusEnum;
use App\Events\TicketCreated;
use App\Http\Requests\Bo\CreateMessage;
use App\Http\Requests\Bo\UpdateMessage;
use Illuminate\Support\Facades\Session;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Database\Eloquent\Builder;
use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\AssignTicketToUserRequest;
use App\Http\Requests\Bo\UpdateTicketPriorityRequest;
use Illuminate\Contracts\Events\Dispatcher;
class TicketController extends Controller
{
/**
* TicketController
*/
public function __construct(
public readonly Dispatcher $dispatcher
) {
parent::__construct();
$this->authorizeResource(Ticket::class, 'ticket');
}
/**
* Tickets List
*
* @param SortTickets $request
* @return \Illuminate\Http\JsonResponse|\Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function index(SortTickets $request)
{
// * Handle Ajax
if ($request->ajax()) {
$tickets = Ticket::with(['user', 'assignedUser']);
if ($request->s) {
$tickets = $tickets->where('text', 'LIKE', "%$request->s%")
->orWhere('id', $request->s)
->orWhereHas('user', function ($q) use ($request) {
$q->where('name', 'LIKE', "%$request->s%");
});
}
/** @var \Illuminate/Pagination/LengthAwarePaginator $tickets */
$tickets = $tickets->paginate();
return \response()->json($tickets->getCollection()->map(function (Ticket $ticket) {
return [
'label' => sprintf(
'Ticket %d (%s): %s',
$ticket->id,
$ticket->user ? $ticket->user->name : trans('Utilisateur supprimé'),
Str::limit(\preg_replace('/[[:cntrl:]]+/', '', $ticket->text), 35)
),
'value' => $ticket->id
];
}));
} //end if
/** @var \App\Models\User */
$user = auth()->user();
$tickets = Ticket::with(['site', 'labels']);
if (!$user->isAdmin()) {
$tickets->whereHas('site', function ($q) use ($user) {
$q->whereHas('users', function ($q) use ($user) {
$q->whereId($user->id);
});
});
$tickets->where('is_internal', false);
} else {
$tickets->has('site');
}
$tickets->where(
'status',
($request->st == TicketStatusEnum::archived->value) ? '=' : '<>',
TicketStatusEnum::archived
);
if (!$user->isAdmin() && $user->sites->count() === 1) {
$request->merge([
'pr' => $user->sites->first()->id,
]);
}
$tickets = $this->filterTickets($tickets, $request);
$tickets = $this->searchTicket($tickets, $request);
/** Sort columns with a query */
$this->sortQuery($tickets);
/** Custom pagination */
$tickets = $this->customPaginate($tickets, $request->pagination);
$sites = Site::select('id', 'name')->orderBy('name')->get();
$status = Arr::sort(TicketStatusEnum::cases(), function (TicketStatusEnum $value) {
return $value->label();
});
$types = Arr::sort(TicketTypeEnum::cases(), function (TicketTypeEnum $value) {
return $value->label();
});
return view('bo.pages.tickets.index', compact('tickets', 'sites', 'status', 'types'));
}
/**
* Show Ticket
*
* @param Request $request
* @param Ticket $ticket
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function show(Request $request, Ticket $ticket)
{
if ($request->ajax()) {
return \response()->json([
'label' => $ticket->formatName($ticket),
'value' => $ticket->id
]);
}
/** @var \App\Models\User */
$user = auth()->user();
if (!$user->can('update', $ticket)) {
abort(403);
}
if ($ticket->is_internal && !$user->can('changeInternalStatus', $ticket)) {
abort(403);
}
$messages = $ticket->messages()->orderBy('created_at', 'DESC')->get();
$isSubscribed = (bool)$ticket->subscriptions()->where('user_id', $user->id)->count();
return view('bo.pages.tickets.show', [
'ticket' => $ticket,
'messages' => $messages,
'isSubscribed' => $isSubscribed
]);
}
/**
* Show create Form
*
* @return \Illuminate\Http\RedirectResponse
*/
public function create()
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user->isAdmin()) {
$sites = Site::orderBy('name')->get();
} else {
$sites = $user->sites;
}
if (!count($sites)) {
return redirect()->route('bo.sites.create')
->with('warning', trans('Vous devez tout d\'abord créer un site pour y ajouter un ticket'));
}
return view('bo.pages.tickets.create', compact('sites'));
}
/**
* Create ticket
*
* @param StoreTicket $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreTicket $request)
{
try {
return DB::transaction(function () use ($request) {
/** @var \App\Models\Site */
$site = Site::findOrFail($request->site_id);
/** @var \App\Models\User */
$user = auth()->user();
$ticket = new Ticket($request->validated());
/** @var array<\Illuminate\Http\UploadedFile> */
$fileTickets = $request->file_tickets ?? [];
if (count($fileTickets) > 0) {
foreach ($fileTickets as $key => $file) {
if ($key === 0) {
$ticket->file = FileStorageHelper::storeFile($ticket, $file);
} else {
$fileNumber = 'file' . $key + 1;
$ticket->$fileNumber = FileStorageHelper::storeFile($ticket, $file);
}
}
}
$ticket->user()->associate($user);
$ticket->site()->associate($site);
$ticket->saveOrFail();
if ($request->labels) {
$ticket->labels()->sync($request->labels);
}
$user->subscriptions()->toggle($ticket->id);
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $site,
ticket: $ticket,
byUserName: $user->name
));
return redirect()->route('bo.ticket.show', $ticket)
->with('success', trans('Ticket enregistré !'));
});
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
} //end try
return \back();
}
/**
* Show update form
*
* @param Ticket $ticket
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function edit(Ticket $ticket)
{
/** @var \App\Models\User */
$user = auth()->user();
if ($user->isAdmin()) {
$sites = Site::orderBy('name')->get();
} else {
$sites = $user->sites;
}
return view('bo.pages.tickets.edit', compact('ticket', 'sites'));
}
/**
* Update Model
*
* @param UpdateTicket $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateTicket $request, Ticket $ticket)
{
$ticket->fill($request->validated());
try {
return DB::transaction(function () use ($request, $ticket) {
/** @var \App\Models\Site */
$site = Site::findOrFail($request->site_id);
/** @var null|array<\Illuminate\Http\UploadedFile> */
$fileTickets = $request->file_tickets;
if (isset($fileTickets) && count($fileTickets) > 0) {
foreach ($fileTickets as $key => $file) {
if ($key === 0) {
$ticket->file = FileStorageHelper::storeFile($ticket, $file);
} else {
$fileNumber = 'file' . $key + 1;
$ticket->$fileNumber = FileStorageHelper::storeFile($ticket, $file);
}
}
}
$ticket->labels()->sync($request->labels);
$ticket->site()->associate($site);
$ticket->saveOrFail();
return redirect()->route('bo.ticket.edit', $ticket->id)
->with('success', 'Modifications enregistrées !');
});
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
} //end try
return \back();
}
/**
* Change ticket Status
*
* @param ChangeTicketStatus $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function changeStatus(ChangeTicketStatus $request, Ticket $ticket)
{
if ($ticket->gitlab_issue) {
\dispatch(new UpdateIssueLabels($ticket, $request->status));
}
$ticket->fill($request->validated());
$ticket->saveOrFail();
return back()
->with('warning', trans('Le status à été modifié'));
}
/**
* Add a new Message to the Ticket
*
* @param CreateMessage $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function createMessage(CreateMessage $request, Ticket $ticket)
{
try {
return DB::transaction(function () use ($request, $ticket) {
/** @var \Illuminate\Http\UploadedFile|null */
$filesMessage = $request->file_messages;
$message = new Message($request->validated());
$message->user()->associate(auth()->user());
$message->ticket()->associate($ticket);
/** @var \App\Models\User */
$user = auth()->user();
// Subscribe to ticket.
if ($request->subscribe) {
$user->subscriptions()->syncWithoutDetaching([$ticket->id]);
} else {
$user->subscriptions()->detach($ticket->id);
}
// Store files if any.
if (isset($filesMessage) and $filesMessage) {
foreach ($filesMessage as $key => $singleFile) {
if ($key === 0) {
$message->file = FileStorageHelper::storeFile($message, $singleFile);
$message->saveOrFail();
} else {
$fileNumber = 'file' . $key + 1;
$message->$fileNumber = FileStorageHelper::storeFile($message, $singleFile);
$message->saveOrFail();
}
}
}
$message->saveOrFail();
return back()
->with('success', trans('Votre message à été ajouté'));
}, 5);
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
} //end try
return back();
}
/**
* Update Message
*
* @param UpdateMessage $request
* @param Ticket $ticket
* @param Message $message
* @return \Illuminate\Http\RedirectResponse
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
*/
public function updateMessage(UpdateMessage $request, Ticket $ticket, Message $message)
{
$message->message = $request->message;
$message->saveOrFail();
return back()
->with('success', trans('Votre message à bien été modifié'));
}
/**
* Change ticket status to archived
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function setToArchive(Ticket $ticket)
{
$ticket->status = TicketStatusEnum::archived;
$ticket->saveOrFail();
return redirect()->route('bo.ticket.index')
->with('warning', trans('Le ticket est archivé'));
}
/**
* Subscribe to ticket
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function subscribe(Ticket $ticket)
{
/** @var \App\Models\User */
$user = auth()->user();
$user->subscriptions()->toggle($ticket->id);
if ($user->subscriptions()->where('ticket_id', $ticket->id)->exists()) {
return back()
->with('success', trans('Vous avez été inscrit'));
}
return back()->with('warning', trans('Vous avez été désinscrit'));
}
/**
* Reopen ticket
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function reopen(Ticket $ticket)
{
$message = trans(':ticketName à réouvert le ticket n°:ticketNumber', [
'ticketName' => $ticket->user->name,
'ticketNumber' => $ticket->id,
]);
/** @var \App\Enums\Tickets\TicketStatusEnum */
$oldStatus = $ticket->status;
$ticket->status = TicketStatusEnum::new;
$ticket->saveQuietly();
event(new TicketStatusChanged($ticket, $oldStatus, null, $message, false, false));
event(new NotifySubscriptions($ticket, $message, collect([$ticket->user])));
/** @var \App\Models\User */
$user = auth()->user();
$user->subscriptions()->toggle($ticket->id);
return back()->with('warning', trans('Votre ticket à été réouvert.'));
}
/**
* Assign ticket to user ticket
*
* @param AssignTicketToUserRequest $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function assignTo(AssignTicketToUserRequest $request, Ticket $ticket)
{
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\User|null */
if ($assignedUser = User::whereId($request->assign)->first()) {
$message = trans(':admin a assigné le ticket n°:ticketNumber à :assignedAdmin', [
'admin' => $user->name,
'ticketNumber' => $ticket->id,
'assignedAdmin' => $assignedUser->name,
]);
$ticket->assignedUser()->associate($assignedUser);
} else {
$message = trans(
':admin a modifié l\'assignation du ticket n°:ticketNumber, il n\'est plus assigné à quelqu\'un.',
[
'admin' => $user->name,
'ticketNumber' => $ticket->id,
]
);
$ticket->assignedUser()->dissociate();
}
$ticket->saveQuietly();
/** @var \App\Enums\Tickets\TicketStatusEnum */
$oldStatus = $ticket->status;
// * Notification systeme doit etre privée.
event(new TicketStatusChanged($ticket, $oldStatus, null, $message, true, false));
event(new NotifySubscriptions($ticket, $message, collect([$ticket->user])));
/** @var \App\Models\User */
$user = auth()->user();
$user->subscriptions()->toggle($ticket->id);
if ($assignedUser) {
return back()->with('info', trans('Le ticket à été assigné à :assignedUser', [
'assignedUser' => $assignedUser->name
]));
}
return back()->with('info', trans('Le ticket n\'est plus assigné à personne'));
}
/**
* Link two tickets.
*
* @param LinkTicket $request
* @param Ticket $ticket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function linkTicket(LinkTicket $request, Ticket $ticket)
{
$linkedTicket = Ticket::findOrFail($request->validated())->first();
if ($ticket->site_id !== $linkedTicket->site_id) {
return back()->with('error', trans('Le ticket que vous avez renseigné ne fait pas partie du même site.'));
}
if (!$ticket->linkedTickets()->where('linked_ticket_id', $linkedTicket->id)->exists()) {
$ticket->linkedTickets()->attach($linkedTicket->id);
$linkedTicket->linkedTickets()->attach($ticket->id);
return back()->with('success', trans('Les tickets ont été liés avec succès.'));
}
return back()->with('error', trans('Les tickets sont déjà liés ensemble.'));
}
/**
* Unlink two tickets.
*
* @param Ticket $ticket
* @param Ticket $linkedTicket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function unlinkTicket(Ticket $ticket, Ticket $linkedTicket)
{
$ticket->linkedTickets()->detach($linkedTicket->id);
$linkedTicket->linkedTickets()->detach($ticket->id);
return back()->with('warning', trans('Le lien entre les tickets à été supprimé.'));
}
// * PRIVATES
/**
* Filter Tickets request constructor
*
* @param Builder $tickets
* @param Request $request
* @return Builder
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
private function filterTickets(Builder $tickets, Request $request): Builder
{
//phpcs:enable
if ($request->rst) {
Session::remove('tickets.ass');
Session::remove('tickets.st');
Session::remove('tickets.pr');
Session::remove('tickets.ty');
Session::remove('tickets.sort_col');
Session::remove('tickets.sort_way');
Session::remove('tickets.or');
Session::remove('tickets.search');
}
if ($request->assr) {
Session::remove('tickets.ass');
}
if ($request->prs) {
Session::remove('tickets.pr');
}
if ($request->sts) {
Session::remove('tickets.st');
}
if ($request->tys) {
Session::remove('tickets.ty');
}
if ($request->sort_col) {
Session::put('tickets.sort_col', $request->sort_col);
}
if ($request->sort_way) {
Session::put('tickets.sort_way', $request->sort_way);
}
if ($request->ord) {
Session::remove('tickets.or');
}
if ($request->pr) {
Session::put('tickets.pr', $request->pr);
}
if ($request->st) {
Session::put('tickets.st', $request->st);
}
if ($request->ty) {
Session::put('tickets.ty', $request->ty);
}
if ($request->ass) {
Session::put('tickets.ass', $request->ass);
}
if (Session::has('tickets.or')) {
Session::remove('tickets.ass');
Session::remove('tickets.st');
Session::remove('tickets.ty');
Session::remove('tickets.sort_col');
Session::remove('tickets.sort_way');
Session::remove('tickets.search');
}
if ($request->or && Session::has('tickets.pr')) {
Session::put('tickets.or', $request->or);
}
Session::save();
if (Session::has('tickets.pr')) {
$tickets->where('site_id', Session::get('tickets.pr'));
}
if (Session::has('tickets.ass')) {
$tickets->whereHas('assignedUser', function ($q) {
$q->whereId(Session::get('tickets.ass'));
});
}
if (Session::has('tickets.st')) {
$tickets->where('status', Session::get('tickets.st'));
}
if (Session::has('tickets.ty')) {
$tickets->where('type', Session::get('tickets.ty'));
}
if (Session::has('tickets.or')) {
$tickets->orderBy('order', 'ASC');
}
if (
Session::has('tickets.sort_col') and
Session::has('tickets.sort_way') and
in_array(Session::get('tickets.sort_col'), [
'type',
'title',
'assigned_to',
'date_mep',
'created_at',
'created_by'
])
) {
$tickets->orderBy(
Session::get('tickets.sort_col'),
Session::get('tickets.sort_way')
);
} else {
$tickets->orderBy('last_activity', 'DESC');
}
return $tickets;
}
/**
* Search ticket sql request constructor
*
* @param Builder $tickets
* @param Request $request
* @return Builder
*/
private function searchTicket(Builder $tickets, Request $request): Builder
{
if ($request->exists('search')) {
if (empty($request->search)) {
Session::remove('tickets.search');
} else {
Session::put('tickets.search', $request->search);
}
}
if (!Session::has('tickets.search')) {
return $tickets;
}
return $this->searchQuery($tickets, Session::get('tickets.search'), function ($q) {
$words = explode(' ', Session::get('tickets.search'));
if (\is_array($words)) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$s = collect(TicketStatusEnum::cases())->filter(function (TicketStatusEnum $t) use ($word) {
return Str::slug($t->label()) === Str::slug($word);
})->map(function (TicketStatusEnum $ticket) {
return $ticket->value;
});
$t = collect(TicketTypeEnum::cases())->filter(function (TicketTypeEnum $s) use ($word) {
return Str::slug($s->label()) === Str::slug($word);
})->map(function (TicketTypeEnum $ticket) {
return $ticket->value;
});
$q->orWhere('id', $word)
->orWhereIn('status', $s)
->orWhereIn('type', $t)
->orWhereHas('user', function ($q) use ($word) {
$q->where('name', 'LIKE', "%$word%");
});
} //end foreach
} //end if
}, 'text', 'title', 'file', 'file2', 'file3', 'file4', 'file5');
}
/**
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request) {
$model = $request->targetModel;
$search = $request->search;
// * Handle Ajax
$tickets = $model::with(['user', 'assignedUser']);
if ($search) {
$tickets = $tickets->where('text', 'LIKE', "%$search%")
->orWhere('id', $search)
->orWhereHas('user', function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%");
})
->orderBy("id", "ASC");
}
return ToolboxHelper::jsonPaginate(
$this->customPaginate($tickets, $request->pagination),
'formatName'
);
});
}
/**
* Create a gitlab issue from a ticket.
*
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function createGitlabIssue(Ticket $ticket)
{
$userGitlabId = auth()->user()->gitlab_id;
$gitlabRepoId = $ticket->site->gitlab_project_id;
if (GitLab::projects()->show($gitlabRepoId)['archived']) {
return back()->with('error', \trans("Ce projet GitLab est archivé et ne peut pas être utilisé."));
}
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug,g-to-dispatch',
TicketTypeEnum::evolution,
TicketTypeEnum::mailing => 'c-feature',
TicketTypeEnum::newsletter => 'c-feature,g-to-estimate',
};
$assigneeId = match ($ticket->type) {
TicketTypeEnum::bug => $userGitlabId,
TicketTypeEnum::evolution,
TicketTypeEnum::mailing,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id
};
try {
foreach ($ticket->getFiles() as $key => $file) {
if (!\is_null($file)) {
$uploadedFile = GitLab::projects()->uploadFile($gitlabRepoId, $file);
$description .= $this->formatGitlabFiles($ticket, $uploadedFile, $key + 1);
}
}
$data = [
'title' => sprintf(
'ts-%s : %s',
$ticket->id,
$ticket->title
),
'description' => $description,
'labels' => \implode(",", [
'c-support',
$cLabel
]),
];
if ($assigneeId) {
$data['assignee_id'] = $assigneeId;
}
$issue = GitLab::issues()->create($gitlabRepoId, $data);
} catch (\Exception $e) {
\report($e);
return back()->with('error', \trans("Une erreur est survenue"));
} catch (\Throwable $e) {
\report($e);
return back()->with('error', \trans("Une erreur est survenue"));
} //end try
$ticket->gitlab_issue = $issue['web_url'];
$ticket->save();
return back()->with(
'success',
trans("L'issue a été créée sur Gitlab.")
);
}
/**
* Format files for Gitlab issues
*
* @param \App\Models\Ticket $ticket
* @param array $uploadedFile
* @param integer $fileNb
* @return string
*/
private function formatGitlabFiles(Ticket $ticket, array $uploadedFile, int $fileNb)
{
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv', 'svg'])) {
$file = \sprintf(
"![Fichier joint %d](%s)",
$fileNb,
$uploadedFile['url']
);
} else {
$file = \sprintf(
'<a href="%s" target="_blank">%s</a>',
$uploadedFile['url'],
__('Lien vers le fichier'),
);
}
return \sprintf("<br><br> Fichier %d : <br>%s<br>", $fileNb, $file);
}
/**
* Change order.
*
* @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request
* @param mixed ...$params
* @return \Illuminate\Http\RedirectResponse
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé.
*/
public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse
{
$paramNames = collect(request()->route()->parameterNames);
$className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly();
$className = "\\App\\Models\\{$className}";
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The {$className} class doesn't exist.");
}
if (!\is_object($model)) {
throw new \RuntimeException("The second last param isn't an object.");
}
if (get_class($model) !== (string)Str::of($className)->replaceStart('\\', '')) {
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Support\Collection $models */
$models = $className::query()
->orderBy('order', 'ASC')
->where('site_id', $model->site_id)
->where('status', '!=', TicketStatusEnum::archived)
->where('id', '!=', $model->getKey())
->where('order', $request->direction ? '<' : '>', $model->order)
->get();
/** @var \Illuminate\Database\Eloquent\Model|null $tmp */
$tmp = $request->direction ? $models->last() : $models->first();
if (!$tmp) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
$newOrder = $tmp->getOriginal('order');
$tmp->update(['order' => $model->getOriginal('order')]);
$model->update(['order' => $newOrder]);
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
/**
* Update ticket priority.
*
* @param UpdateTicketPriorityRequest $request
* @param Ticket $ticket
* @return void
*/
public function changePriority(UpdateTicketPriorityRequest $request, Ticket $ticket)
{
$newPriority = $request->validated('priority');
return DB::transaction(function () use ($ticket, $newPriority) {
// Update all tickets who will be moved with this priority update.
$request = Ticket::query()
->where('site_id', $ticket->site_id)
->whereNot('status', TicketStatusEnum::archived)
->whereNot('id', $ticket->id)
->whereBetween('order', [min($ticket->order, $newPriority), max($ticket->order, $newPriority)]);
$ticket->order < $newPriority ? $request->decrement('order', 1) : $request->increment('order', 1);
// Update the ticket.
$ticket->order = $newPriority;
if (!$ticket->saveOrFail()) {
return back()->with('error', trans('crud.messages.order_not_changed'));
};
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\TicketTypes\UpdateTicketTypeRequest;
use App\Models\Ticket\TicketType;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketTypeController extends Controller
{
/**
* 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 $ticketTypeModels */
$ticketTypeModels = TicketType::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$ticketTypeModels,
$search,
null,
'type',
);
}
$searchFields = trans('Type');
/** Sort columns with a query */
$this->sortQuery($ticketTypeModels);
/** Custom pagination */
$ticketTypeModels = $this->customPaginate($ticketTypeModels, $request->pagination);
return view('bo.pages.ticket_types.index', compact('ticketTypeModels', 'search', 'searchFields'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\Ticket\TicketType $ticketType
* @return \Illuminate\Contracts\View\View
* @ignore phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function edit(TicketType $ticketType): \Illuminate\Contracts\View\View
{
return view('bo.pages.ticket_types.edit', ['ticketTypeModel' => $ticketType]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\TicketTypes\UpdateTicketTypeRequest $request
* @param \App\Models\Ticket\TicketType $ticketType
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateTicketTypeRequest $request, TicketType $ticketType): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $ticketType) {
$ticketType->fill($request->validated());
if ($ticketType->saveOrFail()) {
return redirect()->route('bo.ticket_types.edit', $ticketType->id)
->with('success', __('crud.messages.has_been_updated', [
'model' => trans_choice('models.classes.ticket_types', 1)
]));
}
return redirect()->route('bo.ticket_types.edit', $ticketType->id)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => trans_choice('models.classes.ticket_types', 1)
]));
});
}
}

View file

@ -0,0 +1,232 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Models\Site;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Http\Controllers\Bo\Auth\ForgotPasswordController;
use App\Http\Requests\Bo\Users\FavouriteSiteRequest;
class UserController extends Controller
{
/**
* 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 $userModels */
$userModels = User::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$userModels,
$search,
null,
'name',
'email'
);
}
$searchFields = trans('Label, email');
/** Sort columns with a query */
$this->sortQuery($userModels);
/** Custom pagination */
$userModels = $this->customPaginate($userModels, $request->pagination);
return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function create(User $user): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.users.create', [
'userModel' => $user,
'siteModels' => $siteModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = User::query()->onlyTrashed()->where('email', $request->validated('email'))->first() ?? new User();
$user->fill($request->safe()->except('sites'));
$user->deleted_at = null;
if ($user->saveOrFail()) {
$user->sites()->attach(collect($request->validated('sites'))->pluck('id'));
return redirect()
->route('bo.users.edit', $user)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.users'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.users'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.users.edit', [
'userModel' => $user,
'siteModels' => $siteModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $user) {
$user->fill($request->safe()->except(['sites']));
$user->sites()->sync(collect($request->validated('sites'))->pluck('id'));
if ($user->saveOrFail()) {
return redirect()
->route('bo.users.edit', $user)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.users'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.users'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($user) {
if ($user->deleteOrFail()) {
$user->sites()->detach();
return redirect()
->route('bo.users.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.users'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.users'))]
));
});
}
/**
* Password reset form
*
* @return \Illuminate\Contracts\View\View
*/
public function settings(): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.settings');
}
/**
* Favourite site submit
*
* @param \App\Http\Requests\Bo\Users\FavouriteSiteRequest $request
*
* @return \Illuminate\Http\RedirectResponse
*/
public function favouriteSiteSubmit(FavouriteSiteRequest $request): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User */
$user = auth()->user();
try {
$user->favourite_site_id = $request->favourite_site_id;
$user->saveOrFail();
return redirect()->back()->with("success", trans('crud.messages.favourite_site_changed'));
} catch (\Exception $e) {
\report($e);
return redirect()->back()->with('error', trans('crud.messages.favourite_site_not_changed'));
} catch (\Throwable $e) {
\report($e);
return redirect()->back()->with('error', trans('crud.messages.favourite_site_not_changed'));
}
}
/**
* Password reset submit
*
* @return \Illuminate\Http\RedirectResponse
*/
public function passwordResetSubmit(): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User */
$user = auth()->user();
$request = new Request(['email' => $user->email]);
return app(ForgotPasswordController::class)->sendResetLinkEmail($request);
}
/**
* Get february last day date of the year
*
* @param integer $year
* @return integer 28 or 29
*/
private function getLastDayFebruary(int $year): int
{
if ((is_int($year / 4) && !is_int($year / 100)) || is_int($year / 400)) {
return 29;
}
return 28;
}
}

View file

@ -0,0 +1,247 @@
<?php
namespace App\Http\Controllers;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\Ticket;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/**
* Controller
*/
public function __construct()
{
$this->middleware(function ($request, $next) {
$ignore = [
// * Support Web
9,
// * Gen-Game- Web
15,
// * Store LDC
19,
// * CRM
27,
// * LDC - WEB
55,
];
$ticketsNew = Ticket::where('status', TicketStatusEnum::new);
$ticketsWaiting = Ticket::where('status', TicketStatusEnum::waiting);
$ticketsInProgress = Ticket::whereNotIn(
'status',
[
TicketStatusEnum::new,
TicketStatusEnum::estimateTodo,
TicketStatusEnum::waiting,
TicketStatusEnum::production,
TicketStatusEnum::archived,
TicketStatusEnum::takenInAccount
]
);
/** @var \App\Models\User|null */
$user = Auth::user();
if (!is_null($user) and !$user->isAdmin()) {
$ticketsNew = $ticketsNew->where('is_internal', false)->whereNotIn('site_id', $ignore);
$ticketsWaiting = $ticketsWaiting->where('is_internal', false)->whereNotIn('site_id', $ignore);
$ticketsInProgress = $ticketsInProgress->where('is_internal', false)->whereNotIn('site_id', $ignore);
}
View::share('ticketsNew', $ticketsNew->count());
View::share('ticketsWaiting', $ticketsWaiting->count());
View::share('ticketsInProgress', $ticketsInProgress->count());
return $next($request);
});
}
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(
Builder $query,
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
$table = $query->getModel()->getTable();
foreach ($fields as $field) {
$field = str_replace('?', '\?', $field);
if (is_array($field)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . htmlspecialchars($search) . '%']
);
return $query;
}
// Get models woth enum's label.
if (is_array($this->getSearchValue($query, $search, $field))) {
foreach ($this->getSearchValue($query, $search, $field) as $array) {
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($array->value) . '%']
);
}
return $query;
}
// Get models with relation's name/label.
if (Str::endsWith($field, '_id')) {
$query->orWhereHas(Str::remove('_id', $field), function ($queryRelation) use ($field, $search) {
if (Schema::hasColumn(Str::remove('_id', $field) . 's', 'name')) {
$queryRelation->where('name', 'like', '%' . $search . '%');
} elseif (Schema::hasColumn(Str::remove('_id', $field) . 's', 'label')) {
$queryRelation->where('label', 'like', '%' . $search . '%');
}
});
return $query;
}
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($this->getSearchValue($query, $search, $field)) . '%']
);
} //end foreach
return $query;
}
/**
* Get the search value (on string or Enum).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param string $field
* @return string|array
*/
protected function getSearchValue(Builder $query, string $search, string $field): string|array
{
$cast = $query->getModel()->getCasts()[$field] ?? null;
if (
count($query->getModel()->getCasts()) and // Check if the model has casts.
$cast and // Check if there is cast about specific field.
enum_exists($cast) // Check if the cast is an enum.
) {
/** @var \App\Traits\Enums $cast */
// @phpstan-ignore-next-line
$cases = collect($cast::toArray());
$enum = $cases->filter(function ($item) use ($search) {
return preg_match('/' . Str::lower($search) . '/', Str::lower($item->label));
});
return $enum->toArray();
} else {
return $search;
}
}
/**
* Sort columns with a query
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
Session::put("$rName.sorted", true);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rst) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (
request()->sort_col &&
request()->sort_way &&
Schema::hasColumn($table, request()->sort_col) &&
(!isset($ignore) || (isset($ignore) && !in_array(request()->sort_col, $ignore)))
) {
$query = $query->orderBy("$table." . request()->sort_col, request()->sort_way);
}
return $query;
}
/**
* Customize pagination with cache or config (default).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param integer|null $pagination
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected function customPaginate(Builder $query, $pagination): LengthAwarePaginator
{
if ($pagination) {
Cache::put('pagination', $pagination);
};
$query = $query->paginate(
(Cache::get('pagination')) ? intval(Cache::get('pagination')) : config('pagination.default')
);
return $query;
}
}

70
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,70 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
'htmlsanitize',
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class,
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
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,
'is-admin' => \App\Http\Middleware\IsAdmin::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,
];
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo(Request $request): ?string
{
$logins = [
'backend' => route('login')
];
if (!$request->expectsJson()) {
// * Notify must login
Session::flash('warning', trans('Veuillez vous connecter pour continuer'));
foreach ($request->route()->gatherMiddleware() as $middleware) {
if (\is_string($middleware) and strpos($middleware, 'auth:') !== false) {
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
if ($route) {
return $route;
}
}
}
return route('login');
}
abort(401);
return null;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, \Closure $next, string|array ...$params): Response
{
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (
null|string|array &$input,
string $name,
?string $rootName = null
) use (
$params,
&$cleanInput
): void {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (string|array &$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsAdmin
{
/**
* 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 */
$user = auth()->user();
if ($user->isAdmin()) {
return $next($request);
}
return redirect()->route('bo.home');
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param string|null ...$guards
* @return \Symfony\Component\HttpFoundation\Response
* @throws \RuntimeException If guard is unhandled.
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, \Closure $next, ...$guards): Response
{
// phpcs:enable phpcs:disable
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
switch ($guard) {
case 'backend':
if (!$request->routeIs('bo.*') && !$request->routeIs('login')) {
continue 2;
}
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
}
}
return $next($request);
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class SecureHeaders
{
/**
* Enumerate headers which you do not want in your application's responses.
* Great starting point would be to go check out @Scott_Helme's:
* https://securityheaders.com/
*
* @var array
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, Closure $next): Response
{
// phpcs:enable
View::share('nonce', $nonce = Vite::useCspNonce());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$reportUri = Route::has('cspReportUri') ?
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList);
$response = $next($request);
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
"frame-src 'self' " . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/'
]) . ';',
"font-src 'self' data: https://fonts.gstatic.com;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
'https://cdnjs.cloudflare.com',
// * File Manager
'https://fonts.googleapis.com',
'https://cdn.jsdelivr.net',
'https://use.fontawesome.com',
'https://fonts.googleapis.com'
]) . ';',
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
'https://www.googletagmanager.com',
'https://cdnjs.cloudflare.com',
"$scheme://www.google-analytics.com",
]) . ';'
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri
);
$response->headers->set('Feature-Policy', 'fullscreen \'self\';');
return $response;
}
/**
* Remove unwanted headers
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var integer
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
'/csp-report',
'/brevo-in'
];
}

View file

@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
/**
* Handles backend forgotten password form validation.
*/
class ForgotPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'email' => trans('validation.attributes.email'),
];
}
}

View file

@ -1,111 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use App\Models\User;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Uwil\LaravelBackend\Requests\Auth\LoginRequest as BackendLoginRequest;
/**
* Handles backend login form validation, authentication and rate limiting.
*/
class LoginRequest extends BackendLoginRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
#[\Override]
public function rules(): array
{
return [
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
'password' => [
'required',
'max:255',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
],
'remember' => 'boolean',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException Wrong email.
*/
#[\Override]
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$user = User::query()
->where('email', $this->input('email'))
->first();
if (
! $user
|| ! $user->userPassword
|| ! Hash::check($this->input('password'), $user->userPassword->password)
) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
Auth::guard('backend')->login($user, $this->boolean('remember'));
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException Wrong email.
*/
#[\Override]
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
}

View file

@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
/**
* Handles backend resetting password form validation.
*/
class ResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$passwordRules = Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised();
return [
'token' => 'required|string',
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
'password' => [
'required',
'required_with:password_confirmation',
'confirmed',
'max:255',
$passwordRules,
],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'token' => trans('validation.custom.token'),
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
];
}
}

View file

@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Brands;
use App\Models\Brand;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for creating a new Brand.
*/
class StoreBrandRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::check('create', Brand::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|between:3,255|unique:brands,name',
'sites' => 'nullable|array',
'sites.*' => 'integer|exists:sites,id|distinct',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'name' => trans('validation.custom.name'),
'sites' => trans_choice('models.classes.sites', \INF),
];
}
}

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Brands;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
/**
* Handles authorization and validation for updating an existing Brand.
*/
class UpdateBrandRequest extends StoreBrandRequest
{
/**
* Determine if the user is authorized to make this request.
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('brand'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
#[\Override]
public function rules(): array
{
/** @var \App\Models\Brand $brand */
$brand = $this->route('brand');
return array_merge(parent::rules(), [
'name' => [
'required',
'string',
'between:3,255',
Rule::unique('brands', 'name')->ignore($brand->getRouteKey()),
],
]);
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\InvoiceCompanies;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for updating an existing InvoiceCompany.
*/
class UpdateInvoiceCompanyRequest extends StoreInvoiceCompanyRequest
{
/**
* Determine if the user is authorized to make this request.
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('invoice_company'));
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\InvoicePackages;
use App\Models\InvoicePackage;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for creating a new InvoicePackage.
*/
class StoreInvoicePackageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::check('create', InvoicePackage::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|between:3,255',
'amount' => 'required|numeric|decimal:0,2|between:1,10000',
'published' => 'required|boolean',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'name' => trans('validation.custom.name'),
'amount' => trans('validation.custom.amount'),
'published' => trans('validation.custom.published'),
];
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\InvoicePackages;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for updating an existing InvoicePackage.
*/
class UpdateInvoicePackageRequest extends StoreInvoicePackageRequest
{
/**
* Determine if the user is authorized to make this request.
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('invoice_package'));
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Notices;
use App\Enums\NoticeTypeEnum;
use App\Models\Notice;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rules\Enum;
use Uwil\LaravelBackend\Rules\WysiwygLength;
/**
* Handles authorization and validation for creating a new Notice.
*/
class StoreNoticeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::check('create', Notice::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'title' => 'required|string|between:3,255',
'message' => ['required', 'string', new WysiwygLength()],
'type' => ['required', 'integer', new Enum(NoticeTypeEnum::class)],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'title' => trans('validation.custom.title'),
'message' => trans('validation.custom.message'),
'type' => trans('validation.custom.type'),
];
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Notices;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for updating an existing Notice.
*/
class UpdateNoticeRequest extends StoreNoticeRequest
{
/**
* Determine if the user is authorized to make this request.
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('notice'));
}
}

View file

@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Sites;
use App\Models\Site;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for creating a new Site.
*/
class StoreSiteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::check('create', Site::class);
}
/**
* Prepare the data for validation.
*/
#[\Override]
protected function prepareForValidation(): void
{
$this->merge([
'project_manager_id' => is_array($this->project_manager_id) && count($this->project_manager_id)
? intval($this->project_manager_id[0])
: $this->project_manager_id,
'gitlab_project_id' => is_array($this->gitlab_project_id)
? intval($this->gitlab_project_id[0])
: $this->gitlab_project_id,
'users' => is_array($this->users) ? array_map(intval(...), $this->users) : $this->users,
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|between:3,255|unique:sites,name',
'url_re7' => 'nullable|string|url:http,https|unique:sites,url_re7',
'url_prod' => 'nullable|string|url:http,https|unique:sites,url_prod',
'project_manager_id' => 'required|integer|exists:users,id',
'gitlab_project_id' => 'required|integer|exists:gitlab_projects,id|unique:sites,gitlab_project_id',
'users' => 'nullable|array',
'users.*' => 'integer|exists:users,id|distinct',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
#[\Override]
public function attributes(): array
{
return [
'name' => trans('validation.custom.name'),
'url_re7' => trans('validation.custom.url_re7'),
'url_prod' => trans('validation.custom.url_prod'),
'project_manager_id' => trans('validation.custom.project_manager_id'),
'gitlab_project_id' => trans_choice('models.classes.gitlab-projects', 1),
'users' => trans_choice('uwil-backend::models.classes.users', \INF),
];
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Back\Sites;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
/**
* Handles authorization and validation for updating an existing Site.
*/
class UpdateSiteRequest extends StoreSiteRequest
{
/**
* Determine if the user is authorized to make this request.
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('site'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
#[\Override]
public function rules(): array
{
/** @var \App\Models\Site $site */
$site = $this->route('site');
return array_merge(parent::rules(), [
'name' => [
'required',
'string',
'between:3,255',
Rule::unique('sites', 'name')->ignore($site->getRouteKey()),
],
'url_re7' => [
'required',
'string',
'url:http,https',
Rule::unique('sites', 'url_re7')->ignore($site->getRouteKey()),
],
'url_prod' => [
'required',
'string',
'url:http,https',
Rule::unique('sites', 'url_prod')->ignore($site->getRouteKey()),
],
'gitlab_project_id' => [
'required',
'integer',
'exists:gitlab_projects,id',
Rule::unique('sites', 'gitlab_project_id')->ignore($site->getRouteKey()),
],
]);
}
}

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