Compare commits

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

283 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
004af70c30 Merge branch '107-issue-bug' into 'develop'
fix: 🐛 remove duplicate imports

See merge request ldc-web/projets/support-web!127
2026-03-25 10:51:55 +00:00
noah.lancien
c88f3e8d52 Merge branch '90-invoice-filters' into 'develop'
feat: ✨ add awaiting signature/invoicing filters and auto-set...

See merge request ldc-web/projets/support-web!121
2026-03-25 10:48:36 +00:00
Corentin Huet
fb78d5a77d fix: 🐛 remove duplicate imports 2026-03-25 11:48:29 +01:00
noah.lancien
292456bf14 Merge branch '107-issue-bug' into 'develop'
feat: ✨ add to-dispatch label for bug tickets and change default assignee label

See merge request ldc-web/projets/support-web!120
2026-03-25 10:48:12 +00:00
Corentin Huet
a43c6c95b1 feat: ✨ add to-dispatch label for bug tickets and change default assignee label 2026-03-25 11:44:17 +01:00
noah.lancien
9b2248b146 Merge branch '111-add-column' into 'develop'
feat: ✨ add estimate column to tickets page

See merge request ldc-web/projets/support-web!126
2026-03-24 15:49:59 +00:00
noah.lancien
9ea4786aa8 Merge branch '105-sync-gitlab-support' into 'develop'
Resolve "Liaison labels (Gitlab) et statuts (support tickets)"

See merge request ldc-web/projets/support-web!123
2026-03-24 15:48:12 +00:00
noah.lancien
b77c0908f6 Merge branch '87-ticket-status' into 'develop'
feat: ✨ add new status + sync of g-wip label with in development

See merge request ldc-web/projets/support-web!122
2026-03-24 15:47:40 +00:00
Corentin Huet
721e2200f6 feat: ✨ add estimate column to tickets page 2026-03-13 14:44:36 +01:00
Noah Lancien
cbabb40f83 Merge branch 'develop' into trunk 2026-02-26 12:10:44 +01:00
noah.lancien
e71c0546c5 Merge branch '112-feedback' into 'develop'
fix: 🐛 correct default selected status in filter dropdown

See merge request ldc-web/projets/support-web!125
2026-02-26 11:10:02 +00:00
Corentin Huet
ebeccffac6 fix: 🐛 correct default selected status in filter dropdown 2026-02-26 11:59:21 +01:00
Noah Lancien
870986faa4 Merge branch 'develop' into trunk 2026-02-26 11:15:14 +01:00
noah.lancien
da65fcd12c Merge branch '112-unused-packages' into 'develop'
feat: ✨ add published status filter to packages listing

See merge request ldc-web/projets/support-web!124
2026-02-26 10:14:38 +00:00
Corentin Huet
e0e6147576 feat: ✨ add published status filter to packages listing 2026-02-26 10:20:22 +01:00
Corentin Huet
c7493abea0 feat: ✨ sync support statuses with gitlab 2026-02-11 13:34:43 +01:00
Corentin Huet
95b69e9496 feat: ✨ add new status + sync of g-wip label with in development 2026-02-11 13:21:29 +01:00
Corentin Huet
a3123a2bf2 feat: ✨ add awaiting signature/invoicing filters and auto-set archived status on accounting sent date 2026-02-09 09:39:24 +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
1794076c91 ci: limit jobs history to 1 2026-01-29 12:38:01 +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
3b93ec45c9 Merge branch 'fix-sentry-error' into 'develop'
fix: 🐛 add null checks for webhook fields

See merge request ldc-web/projets/support-web!119
2026-01-19 08:36:56 +00:00
Corentin Huet
3b7ca55d73 fix: 🐛 add null checks for webhook fields 2026-01-16 17:20:25 +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
7e9cba3d2a Merge branch '106-mailing-ticket' into 'develop'
feat: ✨ implement webhook ticket creation with regex extraction

See merge request ldc-web/projets/support-web!118
2026-01-16 14:48:57 +00:00
Corentin Huet
4e275c3b37 feat: ✨ implement webhook ticket creation with regex extraction 2026-01-16 15:21:48 +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
Noah Lancien
37beca9c5e ci: added ci variables to define pod resources 2026-01-12 09:31:48 +01:00
Noah Lancien
7aa279e9e4 feat: updated admin-secure 2026-01-12 09:26:03 +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
Vincent BRIDIER
b14305553f fix: Some PHPCBF 2026-01-02 15:31:21 +01:00
Vincent BRIDIER
aa9483a155 feat: Add Gitlab some datas sync 2026-01-02 15:30:52 +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
90f27bf560 Merge branch 'reorder-priority-fix' into 'develop'
fix: reorder tickets priority when one is archived

See merge request ldc-web/projets/support-web!117
2025-11-21 07:59:18 +00:00
Noah Lancien
ccc4ed596a fix: reorder tickets priority when one is archived 2025-11-20 17:04:08 +01:00
Noah Lancien
3f4342f3bb Merge branch 'develop' into trunk 2025-11-20 15:54:16 +01:00
noah.lancien
e67e4b3cef Merge branch '103-priority' into 'develop'
feat: replaced change-order with a new priority system

See merge request ldc-web/projets/support-web!116
2025-11-20 14:51:51 +00:00
Noah Lancien
c09214f59a fix: use increment/decrement instead of loop 2025-11-20 09:55:38 +01:00
Noah Lancien
950b2babdc feat: replaced change-order with a new priority system 2025-11-19 17:45:29 +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
noah.lancien
13cc9d1394 Merge branch '102-fix-title' into 'develop'
fix: remove title field

See merge request ldc-web/projets/support-web!115
2025-10-30 11:19:07 +00:00
Corentin Huet
b97f7f575e fix: remove title field 2025-10-23 12:00:50 +02:00
K Muller
4bdcf60dfa Merge branch 'develop' into trunk 2025-10-23 10:32:31 +02:00
karl.muller
17ee6de791 Merge branch '101-mailing' into 'develop'
feat: ✨ add new ticket type for mass email requests

See merge request ldc-web/projets/support-web!113
2025-10-23 08:30:36 +00:00
karl.muller
460029cf83 Merge branch '102-ticket-name' into 'develop'
feat: ✨ add ticket title to tickets.show

See merge request ldc-web/projets/support-web!114
2025-10-23 08:30:32 +00:00
Corentin Huet
e54c491c2d feat: ✨ add ticket title to tickets.show 2025-10-21 11:33:43 +02:00
Corentin Huet
51f77d3be8 feat: ✨ add new ticket type for mass email requests 2025-10-21 11:15:54 +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
365b839f53 ci: added staging-resources.yaml 2025-10-09 11:49:53 +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
4434538cc3 Merge branch 'issue-99' into 'develop'
fix: add automatic retries on deadlocks

See merge request ldc-web/projets/support-web!111
2025-10-09 09:38:21 +00: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
noah.lancien
ec62c6d4b5 Merge branch 'issue-98' into 'develop'
feat: add ticket title in mails

See merge request ldc-web/projets/support-web!112
2025-09-18 06:56:43 +00:00
Corentin Huet
0f31e03169 feat: add ticket title in mails 2025-09-16 15:06:12 +02:00
Corentin Huet
849420f966 fix: add automatic retries on deadlocks 2025-09-16 12:19:48 +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
cc339201f0 Merge branch 'issue-84' into 'develop'
fix: 🐛 add another condition to test if the user subscribe to the ticket in Message.php

See merge request ldc-web/projets/support-web!109
2025-08-22 08:27:05 +00:00
nicolas.demangeat
e47d9a75e6 fix: 🐛 Change relationship management for subscriptions - Close #84 2025-08-22 08:27:05 +00:00
Vincent BRIDIER
4d777f6281 feat: Remove unique for gitlab_id on site - Close #94 2025-08-21 12:41:06 +02:00
Vincent BRIDIER
a3529361b7 Merge branch 'issue-95' into 'develop'
fix: 🚑 add Null coalescing operator in projects index.blade to avoid production error

See merge request ldc-web/projets/support-web!110
2025-08-21 10:28:32 +00:00
Vincent BRIDIER
93a4d54a9e feat: Remove legacy Projects parts 2025-08-21 12:24:42 +02:00
Vincent BRIDIER
46bfa5ef1f feat: Update readme 2025-08-20 14:46:50 +00:00
Nicolas Demangeat
005378de41 fix: 🐛 add cascadeOnDelete to project table and change the Project query in index method in ProjectController 2025-08-19 10:27:36 +02:00
Nicolas Demangeat
ed4b227f9a fix: 🚑 add Null coalescing operator in projects index.blade to avoid production error 2025-08-18 15:29:44 +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
88abac9a5d
fix: Fix Requests 2025-08-18 14:16:06 +02:00
Vincent BRIDIER
9172d75e5d
fix: Fix Requests 2025-08-18 14:15:04 +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
21abe7d761
fix: Fix Requests 2025-08-18 12:40:36 +02:00
Vincent BRIDIER
f9ed83ff60
Merge branch 'develop' into trunk 2025-08-18 12:13:24 +02:00
Vincent BRIDIER
df912545e1
feat: Add words blacklist for message and ticket creation 2025-08-18 12:13:04 +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
8f0a139663 Merge branch 'issue-71-favourite' into 'develop'
feat: choice of favourite site added to settings

See merge request ldc-web/projets/support-web!108
2025-07-10 11:04:02 +00:00
Corentin Huet
7ee190f1d3 feat: choice of favourite site added to settings 2025-07-10 12:49:38 +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
c6e5c5eeb7 Merge branch 'issue-74-format' into 'develop'
feat: allow svg files in tickets

See merge request ldc-web/projets/support-web!107
2025-07-09 13:20:57 +00:00
Corentin Huet
3c035aec9d feat: allow svg files in tickets 2025-07-09 15:08:49 +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
dd230c4516 Merge branch 'issue-92-consumer-tickets' into 'develop'
feat: add complaints enum for tickets

See merge request ldc-web/projets/support-web!106
2025-06-24 08:22:07 +00:00
Noah Lancien
6dd7278c4f feat: add complaints enum for tickets 2025-06-19 17:32:33 +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
2d0b300a52 ci: update image version + replace 443 port by 80 in manifest 2025-06-19 12:00:53 +02:00
Noah Lancien
52386bbfd4 Merge branch 'develop' into trunk 2025-06-19 11:48:17 +02:00
noah.lancien
748a5018af Merge branch 'issue-89' into 'develop'
feat: 💩 trying to fix invoices sorting, trying to make the delete cascade for site

See merge request ldc-web/projets/support-web!105
2025-06-19 09:47:47 +00:00
Noah Lancien
fbf4a2f96c fix: MR feedback 2025-06-19 11:46:01 +02:00
Noah Lancien
7089b11c5f fix: linkedTickets() method was called with a wrong name 2025-06-19 11:09:42 +02:00
Nicolas Demangeat
de14d846c5 feat: 💩 trying to fix invoices sorting, trying to make the delete cascade for site 2025-06-18 11:10:46 +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
noah.lancien
77a7871f3d Merge branch 'issue-89' into 'develop'
fix 🐛 sort display invoices is now OK

See merge request ldc-web/projets/support-web!104
2025-06-16 09:50:42 +00:00
Nicolas Demangeat
c63f2b62e9 fix: 🎨 add condition to have invoiceModels sorted by query string or orderByDesc if no query string in InvoiceController 2025-06-16 10:54:46 +02:00
Nicolas Demangeat
4c8f5f20f7 fix: 🐛 add a default behavior in sortQuery method in Controller.php to have the query orderByDesc if no query string 2025-06-16 09:53:32 +02:00
Nicolas Demangeat
09d84e31fb fix 🐛 sort display invoices is now OK 2025-06-13 16:59:39 +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
88457bb44a
feat: Add Tickets state export notification 2025-05-27 10:29:56 +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
7116c5d41f Merge branch 'issue-83' into 'develop'
fix: 🐛 change the blade condition to display or not the project manager

See merge request ldc-web/projets/support-web!103
2025-05-12 13:10:45 +00:00
Nicolas Demangeat
bc0d61d438 fix: ♻️ remove empty class 2025-05-12 15:09:50 +02:00
Nicolas Demangeat
aa49e077bd fix: 💄 refacto tickets show blade class to display the answer button always on the right 2025-05-12 15:08:05 +02:00
Nicolas Demangeat
a484335112 fix: 🐛 change the blade condition to display or not the project manager 2025-05-12 14:39:36 +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
Vincent BRIDIER
420947f8e0 Merge branch 'issue-83' into 'develop'
fix: 🐛 respond button is now displayed

See merge request ldc-web/projets/support-web!102
2025-05-12 09:39:59 +00:00
Nicolas Demangeat
17c10a1618 fix: 🐛 respond button is now displayed 2025-05-12 10:45:38 +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
db39f78281 Merge branch '81-gitlab-id-troubles-in-requests' into 'develop'
fix: allow null value for gitlab_id and fix update request for users

See merge request ldc-web/projets/support-web!101
2025-04-24 13:00:47 +00:00
Noah Lancien
ea29760b95 fix: allow null value for gitlab_id and fix update request for users 2025-04-24 14:52:52 +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
noah.lancien
83d4d5442f Merge branch 'issue-80' into 'develop'
feat: ✨ you can now edit the sent accounting date even when the date...

See merge request ldc-web/projets/support-web!100
2025-04-23 10:10:31 +00:00
Nicolas Demangeat
e66af8dc92 fix: refacto condition 2025-04-23 11:41:38 +02:00
Nicolas Demangeat
e595c7e145 feat: ✨ you can now edit the sent accounting date even when the date is present if you are admin 2025-04-23 11:12:41 +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
152 changed files with 10046 additions and 2787 deletions

View file

@ -30,19 +30,17 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=noreply-sw@ldc.fr
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
MAIL_ADMIN=equipe.web@ldc.fr
MAIL_LIST_SERVICEWEB=digital@ldc.fr
MAIL_ADMIN=digital@ldc.fr
# Sentry
SENTRY_LARAVEL_DSN=
# ADMIN SECURE
# - API ENDPOINT (Optional)
LDC_WEB_API_ENDPOINT=
# - API KEY
LDC_WEB_API_KEY=
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional)
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=
# GITLAB API
GITLAB_URL=

4
.gitignore vendored
View file

@ -22,3 +22,7 @@ Homestead.yaml
npm-debug.log
yarn-error.log
storage/samlidp/*
.npmrc
.zshrc
.npm/
.composer/

View file

@ -7,7 +7,7 @@ include:
file: 'security/security.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'build/build.yml'
file: "build/docker-multistage.yml"
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'test/test.yml'

View file

@ -1,5 +1,43 @@
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:latest
COPY --chown=www-data:www-data . /app/srcs
FROM composer:2.9.2 AS vendor
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM node:20.18 AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
WORKDIR /app
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 --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
USER root
RUN chmod -R 755 /app/srcs/storage
USER www-data
USER ${USER}

View file

@ -2,11 +2,16 @@
PROD : support.uwil.fr
RE7 : support-web.re7-s-web.fr
## Gitlab Intégration
Le token de groupe pour l'intégration est valable 1 an max
Le token de groupe pour l'intégration est valable 11 mois
Il faut le renouveler ici : https://lab.s-web.fr/groups/ldc-web/projets/-/settings/access_tokens et le nommer SW Intégration
Role : Guest
Scope : api
## TODO
- 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

View file

@ -117,7 +117,7 @@ class CreateUser extends Command
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$tmp = $this->ask('Type the wanted user email', 'digital@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);

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

@ -19,7 +19,7 @@ class TestMail extends Command
*
* @var string
*/
protected $description = 'Send test email to equipe.web@ldc.fr';
protected $description = 'Send test email to digital@ldc.fr';
/**
* Execute the console command.
@ -29,7 +29,7 @@ class TestMail extends Command
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('equipe.web@ldc.fr')->subject('Test Email');
$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

@ -3,6 +3,7 @@
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;
@ -24,8 +25,10 @@ class Kernel extends ConsoleKernel
*/
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');
}
/**

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

@ -8,10 +8,12 @@ enum InvoiceStatusEnum: int
{
use BaseEnum;
case opened = 1;
case refused = 2;
case canceled = 3;
case archived = 4;
case opened = 1;
case refused = 2;
case canceled = 3;
case archived = 4;
case awaiting_signature = 5;
case awaiting_invoicing = 6;
/**
* Optionnal labels definition.
@ -21,5 +23,7 @@ enum InvoiceStatusEnum: int
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

@ -16,6 +16,7 @@ enum TicketFileTypeEnum: int
case docx = '.docx';
case xlsx = '.xlsx';
case csv = '.csv';
case svg = '.svg';
/**
* Optionnal labels definition
@ -29,6 +30,7 @@ enum TicketFileTypeEnum: int
self::docx->name => "Fichier docx",
self::xlsx->name => "Fichier xlsx",
self::csv->name => "Fichier csv",
self::svg->name => "Fichier svg",
];
/**
@ -43,6 +45,7 @@ enum TicketFileTypeEnum: int
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',
];
/**

View file

@ -17,6 +17,7 @@ enum TicketStatusEnum: int
case recetteValidated = 7;
case production = 8;
case archived = 9;
case inDev = 10;
/**
* Optionnal labels definition
@ -24,13 +25,14 @@ enum TicketStatusEnum: int
private const LABELS = [
self::new->name => "Nouveau",
self::estimateTodo->name => "Étude chef(s) de projet",
self::takenInAccount->name => "Pris en compte",
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",
];
/**
@ -46,6 +48,7 @@ enum TicketStatusEnum: int
self::recetteValidated->name => 'text-success',
self::production->name => 'text-success',
self::archived->name => 'text-secondary',
self::inDev->name => 'text-discovery',
];
/**

View file

@ -14,6 +14,8 @@ enum TicketTypeEnum: int
case newsletter = 4;
case qrcode = 5;
case advice = 6;
case complaints = 7;
case mailing = 8;
/**
* Optionnal labels definition
@ -25,6 +27,8 @@ enum TicketTypeEnum: int
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",
];
/**
@ -37,6 +41,8 @@ enum TicketTypeEnum: int
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",
];
/**

View file

@ -1,24 +0,0 @@
<?php
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
enum GitlabUsersEnum: int
{
use BaseEnum;
/**
* Each user is associated to his ID on gitlab.
*
* 24 is Nicolas Demangeat
*/
case default = 24;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::default->name => "Default",
];
}

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\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,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

@ -25,12 +25,27 @@ class InvoiceController extends Controller
*/
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 = Invoice::query()->orderByDesc('created_at');
$status = intval($request->status);
$invoiceModels = $invoiceModels->when($status, function ($query) use ($status) {
$query->where('status', $status);
});
$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;
@ -75,7 +90,7 @@ class InvoiceController extends Controller
*/
public function create(Invoice $invoice): \Illuminate\Contracts\View\View
{
$packageModels = Package::get();
$packageModels = Package::where('published', true)->get();
$taskRowModel = Invoice::getTaskStructure();
$siteModels = Site::orderBy('name', 'ASC')->get();
$companyModels = Company::get();

View file

@ -23,6 +23,15 @@ class PackageController extends Controller
/** @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) {
@ -42,7 +51,7 @@ class PackageController extends Controller
/** Custom pagination */
$packageModels = $this->customPaginate($packageModels, $request->pagination);
return view('bo.pages.packages.index', compact('packageModels', 'search', 'searchFields'));
return view('bo.pages.packages.index', compact('packageModels', 'search', 'searchFields', 'status'));
}
/**
@ -153,4 +162,16 @@ class PackageController extends Controller
));
});
}
/**
* 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

@ -1,193 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Projects\StoreProjectRequest;
use App\Http\Requests\Bo\Projects\UpdateProjectRequest;
use App\Lib\Helpers\ToolboxHelper;
use App\Models\Project;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProjectController 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 $projectModels */
$projectModels = Project::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$projectModels,
$search,
null,
'name',
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($projectModels);
/** Custom pagination */
$projectModels = $this->customPaginate($projectModels, $request->pagination);
return view('bo.pages.projects.index', compact('projectModels', 'search', 'searchFields'));
}
/**
* Crud show view
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function show(Project $project)
{
return view('bo.pages.projects.show', [
'projectModel' => $project
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View
*/
public function create(Project $project): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.projects.create', [
'projectModel' => $project,
'siteModels' => $siteModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Projects\StoreProjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreProjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$project = new Project();
$project->fill($request->validated());
if ($project->saveOrFail()) {
return redirect()
->route('bo.projects.edit', $project)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.projects'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View
*/
public function edit(Project $project): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.projects.edit', [
'projectModel' => $project,
'siteModels' => $siteModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Projects\UpdateProjectRequest $request
* @param \App\Models\Project $project
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateProjectRequest $request, Project $project): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $project) {
$project->fill($request->validated());
if ($project->saveOrFail()) {
return redirect()
->route('bo.projects.edit', $project)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.projects'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Project $project
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Project $project): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($project) {
if ($project->deleteOrFail()) {
return redirect()
->route('bo.projects.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.projects'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* 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

@ -28,11 +28,11 @@ class SiteController extends Controller
if ($user->isAdmin()) {
/** @var \Illuminate\Database\Eloquent\Builder $siteModels */
$siteModels = Site::query();
$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);
$query->where('id', $user->id)->with(['invoices', 'tickets']);
});
}

View file

@ -2,44 +2,48 @@
namespace App\Http\Controllers\Bo;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Users\GitlabUsersEnum;
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\AssignTicketToUserRequest;
use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\CreateMessage;
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\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\UpdateMessage;
use App\Http\Requests\Bo\UpdateTicket;
use App\Http\Requests\Bo\LinkTicket;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Mail\NewTicket;
use App\Models\Message;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
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 Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
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 function __construct(
public readonly Dispatcher $dispatcher
) {
parent::__construct();
$this->authorizeResource(Ticket::class, 'ticket');
}
@ -212,7 +216,7 @@ class TicketController extends Controller
}
}
$ticket->user()->associate(auth()->user());
$ticket->user()->associate($user);
$ticket->site()->associate($site);
$ticket->saveOrFail();
@ -222,11 +226,11 @@ class TicketController extends Controller
$user->subscriptions()->toggle($ticket->id);
if (config('mail.MAIL_ADMIN')) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket));
} else {
throw new \ErrorException('Missing MAIL_ADMIN in .env');
}
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $site,
ticket: $ticket,
byUserName: $user->name
));
return redirect()->route('bo.ticket.show', $ticket)
->with('success', trans('Ticket enregistré !'));
@ -312,6 +316,10 @@ class TicketController extends Controller
*/
public function changeStatus(ChangeTicketStatus $request, Ticket $ticket)
{
if ($ticket->gitlab_issue) {
\dispatch(new UpdateIssueLabels($ticket, $request->status));
}
$ticket->fill($request->validated());
$ticket->saveOrFail();
@ -338,15 +346,14 @@ class TicketController extends Controller
/** @var \App\Models\User */
$user = auth()->user();
// Subscribe to ticket.
if ($request->subscribe) {
if (!$user->subscriptions()->find($ticket->id)) {
$user->subscriptions()->attach($ticket->id);
}
} elseif ($user->subscriptions()->find($ticket->id)) {
$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) {
@ -364,7 +371,7 @@ class TicketController extends Controller
return back()
->with('success', trans('Votre message à été ajouté'));
});
}, 5);
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
@ -517,9 +524,9 @@ class TicketController extends Controller
return back()->with('error', trans('Le ticket que vous avez renseigné ne fait pas partie du même site.'));
}
if (!$ticket->linkedTicket()->where('linked_ticket_id', $linkedTicket->id)->exists()) {
$ticket->linkedTicket()->attach($linkedTicket->id);
$linkedTicket->linkedTicket()->attach($ticket->id);
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.'));
}
@ -536,8 +543,8 @@ class TicketController extends Controller
*/
public function unlinkTicket(Ticket $ticket, Ticket $linkedTicket)
{
$ticket->linkedTicket()->detach($linkedTicket->id);
$linkedTicket->linkedTicket()->detach($ticket->id);
$ticket->linkedTickets()->detach($linkedTicket->id);
$linkedTicket->linkedTickets()->detach($ticket->id);
return back()->with('warning', trans('Le lien entre les tickets à été supprimé.'));
}
@ -745,17 +752,26 @@ class TicketController extends Controller
*/
public function createGitlabIssue(Ticket $ticket)
{
$userGitlabId = auth()->user()->gitlab_id;
$gitlabRepoId = $ticket->site->gitlab_project_id;
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug',
TicketTypeEnum::evolution, TicketTypeEnum::newsletter => 'c-feature',
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 => GitlabUsersEnum::default->value,
TicketTypeEnum::bug => $userGitlabId,
TicketTypeEnum::evolution,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id ?? GitlabUsersEnum::default->value
TicketTypeEnum::mailing,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id
};
try {
@ -766,7 +782,7 @@ class TicketController extends Controller
}
}
$issue = GitLab::issues()->create($gitlabRepoId, [
$data = [
'title' => sprintf(
'ts-%s : %s',
$ticket->id,
@ -777,8 +793,13 @@ class TicketController extends Controller
'c-support',
$cLabel
]),
'assignee_id' => $assigneeId,
]);
];
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"));
@ -806,7 +827,7 @@ class TicketController extends Controller
*/
private function formatGitlabFiles(Ticket $ticket, array $uploadedFile, int $fileNb)
{
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv'])) {
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv', 'svg'])) {
$file = \sprintf(
"![Fichier joint %d](%s)",
$fileNb,
@ -879,4 +900,34 @@ class TicketController extends Controller
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

@ -2,15 +2,16 @@
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Bo\Auth\ForgotPasswordController;
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\Models\Site;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\Http\Controllers\Bo\Auth\ForgotPasswordController;
use App\Http\Requests\Bo\Users\FavouriteSiteRequest;
class UserController extends Controller
{
@ -72,8 +73,9 @@ class UserController extends Controller
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = new User();
$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'));
@ -170,9 +172,35 @@ class UserController extends Controller
*
* @return \Illuminate\Contracts\View\View
*/
public function passwordReset(): \Illuminate\Contracts\View\View
public function settings(): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.passwordReset');
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'));
}
}
/**

View file

@ -216,13 +216,13 @@ class Controller extends BaseController
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->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;
}

View file

@ -28,7 +28,7 @@ class RedirectIfAuthenticated
switch ($guard) {
case 'backend':
if (!$request->routeIs('bo.*') && !$request->routeIs('login')) {
continue;
continue 2;
}
return \redirect()->route('bo.dashboard');
default:

View file

@ -12,6 +12,7 @@ class VerifyCsrfToken extends Middleware
* @var array<int, string>
*/
protected $except = [
'/csp-report'
'/csp-report',
'/brevo-in'
];
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Requests\Bo;
use Domain\Models\ValueObjects\Email;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Config;
class BrevoWebhookRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function getFromAddress(): Email
{
return new Email($this->string('items.0.From.Address'));
}
public function getToAddress(): Email
{
return new Email((string) $this->input('items.0.To.0.Address'));
}
public function getRawHtmlBody(): string
{
return $this->string('items.0.RawHtmlBody');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$expectedFormAddress = Config::string('mail.brevo_webook.expected_from_address');
return [
'items' => ['required', 'array', 'min:1'],
'items.0.From.Address' => ['required', 'email:rfc,spoof'],
'items.0.To.*.Address' => ['required', 'email:rfc,spoof', "in:{$expectedFormAddress}"],
'items.0.RawHtmlBody' => ['required', 'string', 'min:10'],
];
}
}

View file

@ -37,6 +37,8 @@ class CreateMessage extends FormRequest
*/
protected function prepareForValidation()
{
/** @var \App\Models\User */
$user = auth()->user();
$this->merge([
'subscribe' => $this->subscribe ? true : false,
'type' => $this->is_private ? MessageTypeEnum::private->value : MessageTypeEnum::public->value
@ -54,9 +56,22 @@ class CreateMessage extends FormRequest
$user = auth()->user();
return [
'subscribe' => 'required|boolean',
'message' => 'required|min:' . ($user->isAdmin() ? '5' : '20'),
'message' => [
'required',
'min:' . ($user->isAdmin() ? '5' : '20'),
function ($attribute, $value, $fail) {
$forbidden = ['merci de', 'merci de'];
foreach ($forbidden as $word) {
if (stripos(strtolower($value), $word) !== false) {
$fail(
"Le champs 'Message' contient la chaine de caractère non autorisée suivante : $word"
);
}
}
},
],
'file_messages' => 'sometimes|array',
'file_messages.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf|max:2048',
'file_messages.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf,svg|max:2048',
'type' => ['required', new Enum(MessageTypeEnum::class)]
];
}

View file

@ -17,6 +17,18 @@ class StorePackageRequest extends FormRequest
return Gate::allows('isAdmin');
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'published' => $this->boolean('published'),
]);
}
/**
* Get the validation rules that apply to the request.
*
@ -27,6 +39,7 @@ class StorePackageRequest extends FormRequest
return [
'name' => 'required|string|min:3|max:255',
'amount' => 'required|numeric|min:0|regex:/^\d+(\.\d{1,2})?$/',
'published' => 'required|boolean',
];
}
@ -40,6 +53,7 @@ class StorePackageRequest extends FormRequest
return [
'name' => trans('Nom'),
'amount' => trans('Montant (par jour)'),
'published' => trans('Publication')
];
}
}

View file

@ -1,32 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Projects;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return Gate::allows('isAdmin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string|min:3|max:255|unique:projects,name',
'site_id' => 'required|exists:sites,id',
];
}
}

View file

@ -1,18 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Projects;
class UpdateProjectRequest extends StoreProjectRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(parent::rules(), [
'name' => 'required|string|min:3|max:255|unique:projects,name,' . $this->route('project')->id . ',id',
]);
}
}

View file

@ -28,7 +28,7 @@ class StoreSiteRequest extends FormRequest
'name' => 'required|string|min:3|max:255',
'users' => 'nullable|array',
'users.*.id' => 'required|exists:users,id',
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id',
'gitlab_project_id' => 'nullable',
'project_manager_id' => 'nullable|exists:users,id',
'url_re7' => 'nullable|url',
'url_prod' => 'nullable|url'

View file

@ -4,15 +4,4 @@ namespace App\Http\Requests\Bo\Sites;
class UpdateSiteRequest extends StoreSiteRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(parent::rules(), [
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id,' . $this->route('site')->id . ',id',
]);
}
}

View file

@ -97,10 +97,24 @@ class StoreTicket extends FormRequest
}
],
'date_mep' => 'nullable|date',
'text' => 'required|string|min:40',
'text' => [
'required',
'string',
'min:40',
function ($attribute, $value, $fail) {
$forbidden = ['merci de', 'merci de'];
foreach ($forbidden as $word) {
if (stripos(strtolower($value), $word) !== false) {
$fail(
"Le champs 'Description du ticket' contient la chaine de caractère non autorisée suivante : $word"
);
}
}
},
],
'title' => 'required|string|between:3,255',
'file_tickets' => 'sometimes|array',
'file_tickets.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf|max:2048',
'file_tickets.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf,svg|max:2048',
'is_internal' => 'required|boolean',
'labels' => 'sometimes|array',
'labels.*' => [

View file

@ -39,5 +39,4 @@ class UpdateChangeOrderRequest extends FormRequest
'direction' => 'required|boolean',
];
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Http\Requests\Bo;
use App\Models\Ticket;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTicketPriorityRequest extends FormRequest
{
private \App\Models\Ticket $ticket;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->ticket = $this->route('ticket');
$max = Ticket::query()->where('site_id', $this->ticket->site_id)->max('order');
$this->merge([
'priority' => min($this->integer('priority'), $max)
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'priority' => [
'required',
'integer',
'min:1',
function ($attribute, $value, $fail) {
if ($this->ticket && intval($value) === $this->ticket->order) {
$fail("La valeur de {$attribute} doit être différente de la valeur actuelle.");
}
},
],
];
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Bo\Users;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Http\FormRequest;
class FavouriteSiteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Auth::check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
/** @var \App\Models\User|null $user */
$user = Auth::user();
return [
'favourite_site_id' => [
'nullable',
'exists:sites,id',
Rule::in($user->sites->pluck('id')),
],
];
}
}

View file

@ -19,6 +19,18 @@ class StoreUserRequest extends FormRequest
return Gate::allows('isAdmin');
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'gitlab_id' => \boolval($this->input('gitlab_id')) ? $this->input('gitlab_id') : null
]);
}
/**
* Get the validation rules that apply to the request.
*
@ -28,7 +40,11 @@ class StoreUserRequest extends FormRequest
{
return [
'name' => 'required|string|min:3|max:255',
'email' => 'email|required|unique:users,email',
'email' => [
'email',
'required',
Rule::unique('users', 'email')->whereNull('deleted_at')
],
'role' => [
'required',
Rule::in(collect(RoleEnum::cases())->pluck('value')->all()),

View file

@ -12,7 +12,8 @@ class UpdateUserRequest extends StoreUserRequest
public function rules()
{
return array_merge(parent::rules(), [
'email' => 'email|required|unique:users,email,' . $this->route('user')->id . ',id',
'email' => 'email|required|unique:users,email,' . $this->route('user')->id . ',id',
'gitlab_id' => 'nullable|integer|unique:users,gitlab_id,' . $this->route('user')->id . ',id'
]);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace App\Jobs;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\Site;
use App\Models\Ticket;
use App\Notifications\ExportTicketsStateNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Notification;
class ExportTicketsState implements ShouldQueue
{
use Queueable;
/**
* CSV File headers
*
* @var array
*/
private $header = [
"Priorité service",
"Site",
"Numéro de ticket",
"Priorité",
"Date de MEP souhaitée",
"Type de ticket",
"Titre du ticket",
"Statut du ticket",
"Chef de projet",
"Utilisateur assigné",
"Date de dernière activité",
"Date de création",
"Lien vers le ticket",
"Lien vers l'issue GitLab"
];
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
// L'ordre commence à 0 et reprendra à 1 pour chaque site.
$order = 0;
// Dernier site traité.
$lastSiteId = null;
// Insertion de la ligne d'en-têtes.
$csvlines[] = $this->header;
// Récupération des tickets.
Ticket::query()
->whereNotIn(
'status',
[TicketStatusEnum::archived, TicketStatusEnum::production]
)
->orderBy(Site::select('name')
->whereColumn('sites.id', 'tickets.site_id'))
->orderBy('order')->get()->each(function (Ticket $ticket) use (&$csvlines, &$lastSiteId, &$order) {
$csvlines[] = [
"",
$ticket->site->name,
$ticket->id,
$lastSiteId === $ticket->site_id ? $order + 1 : 1,
$ticket->date_mep?->format('d-m-Y') ?? 'No MEP Date',
$ticket->type->label(),
$ticket->title,
$ticket->status->label(),
$ticket->projectManager->name ?? 'No PM',
$ticket->assignedUser()->name ?? 'No Assigned User',
Carbon::create($ticket->getRawOriginal('last_activity'))->format('d-m-Y H:i:s'),
$ticket->created_at->format('d-m-Y H:i:s'),
route('bo.ticket.show', $ticket->id),
$ticket->gitlab_issue ?? 'No GitLab Issue'
];
$order = $lastSiteId === $ticket->site_id ? $order + 1 : 1;
$lastSiteId = $ticket->site_id;
});
Notification::route('mail', config('mail.MAIL_ADMIN'))
->notify(new ExportTicketsStateNotification($this->array2csv($csvlines, ";")));
}
/**
* converts an array to a csv stream
*
* @param array $data
* @param string $delimiter
* @param string $enclosure
* @param string $escape_char
*
* @return string
*/
protected function array2csv(
array $data,
string $delimiter = ',',
string $enclosure = '"',
string $escape_char = "\\"
) {
$f = fopen('php://memory', 'r+');
// UTF-8 BOM.
$BOM = "\xEF\xBB\xBF";
fwrite($f, $BOM);
foreach ($data as $item) {
fputcsv($f, $item, $delimiter, $enclosure, $escape_char);
}
rewind($f);
return stream_get_contents($f);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Jobs;
use App\Models\Ticket;
use App\Enums\Tickets\TicketStatusEnum;
use Illuminate\Foundation\Queue\Queueable;
use App\Traits\Controllers\HasGitlabIssue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Http\Requests\Bo\ChangeTicketStatus;
class UpdateIssueLabels implements ShouldQueue
{
use Queueable;
use HasGitlabIssue;
/** @var Ticket */
protected $ticket;
/** @var requestStatus */
protected $requestStatus;
/**
* Create a new job instance.
*
* @param \App\Models\Ticket $ticket
* @param string $request
*
* @return void
*/
public function __construct(Ticket $ticket, string $requestStatus)
{
$this->ticket = $ticket;
$this->requestStatus = $requestStatus;
}
/**
* Execute the job.
*/
public function handle(): void
{
$oldStatus = $this->ticket->status;
$requestStatus = (int) $this->requestStatus;
$gitlabIssue = $this->getGitlabIssue($this->ticket->site->gitlab_project_id, $this->ticket->gitlab_issue);
$issueLabels = $gitlabIssue['labels'];
if ($oldStatus === TicketStatusEnum::waiting && $requestStatus !== TicketStatusEnum::waiting->value) {
$issueLabels = array_values(array_diff($issueLabels, ['g-wait']));
}
switch ($requestStatus) {
case TicketStatusEnum::takenInAccount->value:
$newLabels = ['g-to-dispatch'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::waiting->value:
$newLabels = ['g-wait'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::waitingValidation->value:
$removeLabels = ['g-to-estimate'];
$updatedLabels = array_diff($issueLabels, $removeLabels);
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::recetteValidated->value:
$newLabels = ['g-re7-ok', 'to-prod'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::archived->value:
$updatedIssue = $this->closeGitlabIssue($gitlabIssue['project_id'], $gitlabIssue['iid'], $issueLabels);
break;
default:
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $issueLabels);
break;
}
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace App\Listeners;
use App\Events\BrevoWebhookCallReceived;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Repositories\SiteRepository;
use App\Services\AutomaticTicketCreator;
use Domain\Models\ValueObjects\Email;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Psr\Log\LoggerInterface;
// NOTE: this should be queued in order to be eventually retried or at least keep info data.
class CreateTicketAutomatically implements ShouldQueue
{
private const string SITE_NAME_WHERE_TO_CREATE_TICKET = 'Mailing en masse';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomaticTicketCreator $automaticTicketCreator,
private readonly SiteRepository $siteRepository,
private readonly Dispatcher $dispatcher
) {}
public function handle(BrevoWebhookCallReceived $event): void
{
$fromEmail = $event->from;
$toEmail = $event->to;
$htmlRawContent = $event->htmlRawContent;
$this->logger->info('Received a ticket creation event from brevo');
$this->logger->debug(
'Payload for ticket creation form brevo',
[
'from_email' => $fromEmail->value,
'to_email' => $toEmail->value,
'html_raw_content' => base64_encode($htmlRawContent)
]
);
$siteReceivingTickets = $this->siteRepository->findByName(self::SITE_NAME_WHERE_TO_CREATE_TICKET);
if ($siteReceivingTickets === null) {
throw new \UnexpectedValueException(
'No site found with name \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$projectManager = $siteReceivingTickets->projectManager()->first();
if ($projectManager === null) {
throw new \UnexpectedValueException(
'No project manager found for site \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$ticket = $this->automaticTicketCreator->createTicket(
$siteReceivingTickets,
$projectManager,
$htmlRawContent,
);
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $siteReceivingTickets,
ticket: $ticket,
byUserName: $fromEmail->value
));
}
}

View file

@ -3,21 +3,51 @@
namespace App\Listeners;
use App\Events\NotifyAdmin;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Mail\TicketUpdate;
use Domain\Models\ValueObjects\Email;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
class SendNotificationMailToAdmin
{
private readonly Email $mailAdmin;
public function __construct()
{
$this->mailAdmin = new Email(
Config::string('mail.MAIL_ADMIN')
);
}
/**
* Handle the event.
*
* @param \App\Events\NotifyAdmin $event
* @return void
*/
public function handle(NotifyAdmin $event)
public function handle(NotifyAdmin | TicketCreated $event)
{
$ticket = $event->ticket;
Mail::to(config('mail.MAIL_ADMIN'))
->send(new TicketUpdate($ticket, $event->corpsMaj));
if ($event instanceof NotifyAdmin) {
$ticket = $event->ticket;
Mail::to($this->mailAdmin->value)
->send(new TicketUpdate($ticket, $event->corpsMaj));
}
if ($event instanceof TicketCreated) {
$ticket = $event->ticket;
$siteReceivingTickets = $event->siteReceivingTicket;
$userName = $event->byUserName;
Mail::to($this->mailAdmin->value)
->send(new NewTicket(
ticketId: $ticket->id,
siteName: $siteReceivingTickets->name,
ticketTitle: $ticket->title,
ticketType: $ticket->type,
userName: $userName,
ticketDescription: $ticket->text,
));
}
}
}

View file

@ -2,29 +2,23 @@
namespace App\Mail;
use App\Models\Ticket;
use App\Enums\Tickets\TicketTypeEnum;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NewTicket extends Mailable
class NewTicket extends Mailable implements ShouldQueue
{
use Queueable;
use SerializesModels;
/** @var \App\Models\Ticket */
private $ticket;
/**
* Create a new message instance.
*
* @param \App\Models\Ticket $ticket
* @return void
*/
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
public function __construct(
private readonly int $ticketId,
private readonly string $siteName,
private readonly string $ticketTitle,
private readonly TicketTypeEnum $ticketType,
private readonly string $userName,
private readonly string $ticketDescription,
) {}
/**
* Build the message.
@ -33,8 +27,14 @@ class NewTicket extends Mailable
*/
public function build()
{
$ticket = $this->ticket;
return $this->subject("[Support] Nouveau ticket n°" . $ticket->id)
->view('emails.new-ticket', compact('ticket'));
return $this->subject("[Support] Nouveau ticket n° {$this->ticketId} - {$this->ticketTitle}")
->view('emails.new-ticket', [
'ticketId' => $this->ticketId,
'siteName' => $this->siteName,
'ticketTitle' => $this->ticketTitle,
'ticketType' => $this->ticketType,
'userName' => $this->userName,
'ticketDescription' => $this->ticketDescription,
]);
}
}

View file

@ -41,7 +41,7 @@ class TicketMention extends Mailable
{
$ticket = $this->ticket;
$corpsMaj = ToolboxHelper::plainToHtml($this->corpsMaj);
return $this->subject("[Support] Vous avez été mentionné sur le ticket n° $ticket->id")
->view('emails.ticket-mention', compact('ticket', 'corpsMaj'));
return $this->subject("[Support] Vous avez été mentionné sur le ticket n°$ticket->id - $ticket->title")
->view('emails.ticket-mention', compact('ticket', 'corpsMaj'));
}
}

View file

@ -41,7 +41,7 @@ class TicketUpdate extends Mailable
{
$ticket = $this->ticket;
$corpsMaj = \html_entity_decode(ToolboxHelper::plainToHtml($this->corpsMaj));
return $this->subject("[Support][{$ticket->site->name}] Ticket n°$ticket->id")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
return $this->subject("[Support][{$ticket->site->name}] Ticket n°$ticket->id - $ticket->title")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
}
}

View file

@ -7,7 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $name
* @property int $id Doit correspondre aux ids marques de la CRM
* @property string $name Nom de la marque
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Account> $accounts
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Site> $sites
* @mixin \Eloquent
*/
class Brand extends Model
{

View file

@ -7,7 +7,14 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* @property int $id
* @property string $name
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read mixed $value
* @method static \Database\Factories\CompanyFactory factory($count = null, $state = [])
* @mixin \Eloquent
*/
class Company extends Model
{

View file

@ -7,10 +7,16 @@ use App\Models\Brand;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $label
* @property AccountTypeEnum $type
* @property integer $balance
* @property Brand $brand_id
* @property int $id
* @property AccountTypeEnum $type Enum Credit\AccountTypeEnum
* @property string $label Custom account label
* @property float $balance Account balance
* @property int $brand_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read Brand $brand
* @property-read \App\Models\Credit\CreditLog|null $creditlogs
* @mixin \Eloquent
*/
class Account extends Model
{

View file

@ -6,10 +6,15 @@ use App\Enums\Credit\BalanceOperationEnum;
use Illuminate\Database\Eloquent\Model;
/**
* @property BalanceOperationEnum $type
* @property double $amount
* @property Account $credit_account_id
* @property date $created_at
* @property int $id
* @property BalanceOperationEnum $type Enum Credit\BalanceOperationEnum
* @property float $amount Montant de l'opération
* @property int $credit_account_id
* @property string $created_at
* @property-read \App\Models\Credit\Account $account
* @property-read \App\Models\Credit\EstimationLog|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit\Estimation> $estimation
* @mixin \Eloquent
*/
class CreditLog extends Model
{

View file

@ -9,6 +9,22 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
/**
* @property int $id
* @property EstimationStatusEnum $status Enum Credit\EstimationStatusEnum
* @property numeric $amount Estimation amount
* @property int $ticket_id
* @property int $credit_account_id The account to debit.
* @property int $estimator_id The person who has done the estimate.
* @property int|null $validator_id The person who has validate the estimate.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Credit\Account $account
* @property-read User|null $estimator
* @property-read Ticket|null $ticket
* @property-read User|null $validator
* @mixin \Eloquent
*/
class Estimation extends Model
{
/**

View file

@ -4,6 +4,14 @@ namespace App\Models\Credit;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $credit_log_id
* @property int $credit_estimation_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class EstimationLog extends Pivot
{
/**

View file

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Gitlab issue title
* @property string $url Gitlab URL
* @property int $estimate_time Gitlab issue estimate time in seconds
* @property int $spent_time Gitlab issue spent time in seconds
* @property int $is_closed Gitlab issue status
* @property int $gitlab_id Gitlab issue id
* @property int $gitlab_iid Gitlab issue iid in project
* @property int|null $milestone_iid Gitlab issue linked milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab issue created at
* @property string $g_updated_at Gitlab issue updated at
* @property string|null $g_closed_at Gitlab issue closed at
* @property string|null $g_due_date Gitlab issue due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabLabels> $gitlabLabels
* @mixin \Eloquent
*/
class GitlabIssues extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'title',
'url',
'estimate_time',
'spent_time',
'is_closed',
'gitlab_id',
'gitlab_iid',
'milestone_iid',
'gitlab_project_id',
'g_created_at',
'g_updated_at',
'g_closed_at',
'g_due_date'
];
/**
* Gitlab Labels relationship
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function gitlabLabels()
{
return $this->belongsToMany(GitlabLabels::class, 'gitlab_issues_labels', 'issue_id', 'label_id')
->withTimestamps();
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Label name
* @property int $gitlab_id Label Gitlab id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabIssues> $issues
* @mixin \Eloquent
*/
class GitlabLabels extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'name',
'gitlab_id'
];
/**
* Gitlab Issues relationship
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function issues()
{
return $this->belongsToMany(GitlabIssues::class, 'gitlab_issues_labels', 'label_id', 'issue_id')
->withTimestamps();
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Milestone title
* @property string $url Gitlab URL
* @property int $is_closed Gitlab milestone status
* @property int $gitlab_id Gitlab milestone id
* @property int $gitlab_iid Gitlab milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab milestone created at
* @property string $g_updated_at Gitlab milestone updated at
* @property string|null $g_start_date Gitlab milestone start date
* @property string|null $g_due_date Gitlab milestone due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabMilestones extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'title',
'url',
'is_closed',
'gitlab_id',
'gitlab_iid',
'gitlab_project_id',
'g_created_at',
'g_updated_at',
'g_start_date',
'g_due_date'
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Gitlab project name
* @property string $url Gitlab URL
* @property int $is_archived Gitlab archive status
* @property int $gitlab_id Gitlab unique ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabProjects extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'name',
'url',
'is_archived',
'gitlab_id'
];
}

View file

@ -7,9 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title
* @property string $message
* @property InfoColorsEnum $type
* @property InfoColorsEnum $type Defined by the InfoColorsEnum enum.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class Information extends Model
{

View file

@ -10,28 +10,42 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property string $ref
* @property string $firstname
* @property string $lastname
* @property string $object
* @property string $tasks
* @property double $total
* @property double $total_days
* @property string $comment
* @property boolean $show_days
* @property date $signed_date
* @property date $sent_client_date
* @property date $sent_accounting_date
* @property Site $site_id
* @property Company $company_id
* @property Company $invoiced_to_id
* @property InvoiceStatusEnum $status
* @property int $id
* @property string $ref The invoice reference built as YYYY_MM_INCREMENT_USER. INCREMENT is the N invoice of the month. USER is the first two letters of the user who last edited the invoice. This field has to generated only on create.
* @property string $firstname The person to which the invoice is addressed.
* @property string $lastname The person to which the invoice is addressed.
* @property string $object The invoice object.
* @property array<array-key, mixed> $tasks A JSON object, each line has to be contructed using these fields description, days, type Invoices\TaskTypeEnum, amount
* @property float $total The total calculated using all tasks
* @property float $total_days The total days calculated using all tasks
* @property string|null $comment Any comment joined to the invoice
* @property int $show_days Show days on the printed invoice
* @property Carbon|null $signed_date The date the invoice was signed
* @property Carbon|null $sent_client_date The date the invoice was send to the client
* @property Carbon|null $sent_accounting_date The date the invoice was send to the client
* @property int $site_id
* @property int $company_id
* @property int $invoiced_to_id
* @property InvoiceStatusEnum $status Status of the invoice.
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \App\Models\Company $company
* @property-read \App\Models\Company $companyInvoiced
* @property-read \App\Models\Site|null $site
* @method static \Database\Factories\InvoiceFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withoutTrashed()
* @mixin \Eloquent
*/
class Invoice extends Model
{
use HasFactory;
use SoftDeletes;
/**
* The attributes that are fillable.
@ -109,6 +123,10 @@ class Invoice extends Model
->reduce(function ($previous, array $task) {
return \floatval($task['amount']) + \floatval($previous);
});
if($model->sent_accounting_date && $model->status !== InvoiceStatusEnum::archived->value) {
$model->status = InvoiceStatusEnum::archived;
}
});
static::deleting(function () {
throw new \Error('Action impossible');

View file

@ -7,6 +7,14 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Un ou deux mot désignant le label
* @property string $color Cast HTML color
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @mixin \Eloquent
*/
class Label extends Model
{
use HasFactory;

View file

@ -9,12 +9,20 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
*
* @property string $message The message content.
* @property \App\Enums\MessageTypeEnum $type The message is not visible for clients.
* @property string $file The message joined file path.
*
* @package App\Models
* @property int $id
* @property int $ticket_id
* @property int|null $user_id
* @property string $message
* @property MessageTypeEnum $type Message Type defined by an enum.
* @property string|null $file
* @property string|null $file2 Pièce jointes n°2
* @property string|null $file3 Pièce jointes n°3
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Ticket|null $ticket
* @property-read \App\Models\User|null $user
* @mixin \Eloquent
*/
class Message extends Model
{
@ -96,14 +104,9 @@ class Message extends Model
}
// ? Is message author is admin ?
if ($messageUser->isAdmin()) {
// Url: https://supportweb.ldc.fr/ticket/583/message/create.
// SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '583-8' for key 'PRIMARY'
// $ticket->subscriptions()->attach($messageUser);
// Add subscription.
if (!$messageUser->subscriptions()->find($ticket->id)) {
$messageUser->subscriptions()->attach($ticket->id);
\session()->flash('info', trans('Vous avez été inscrit.'));
}
$messageUser->subscriptions()->syncWithoutDetaching([$ticket->id]);
\session()->flash('info', trans('Vous avez été inscrit.'));
}
// * Send mail to other subscribed users
event(new NotifySubscriptions(

View file

@ -6,8 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name
* @property double $amount
* @property float $amount The cost per day
* @property bool $published
* @method static \Database\Factories\PackageFactory factory($count = null, $state = [])
* @mixin \Eloquent
*/
class Package extends Model
{
@ -28,5 +32,16 @@ class Package extends Model
protected $fillable = [
'name',
'amount',
'published'
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'published' => 'boolean',
];
}

View file

@ -1,37 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $name
* @property Site $site_id
*/
class Project extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'site_id'
];
// * RELATIONSHIPS.
/**
* BelongsTo Site
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function site()
{
return $this->belongsTo(Site::class);
}
}

View file

@ -10,11 +10,26 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property string $name
* @property string $url_re7
* @property string $url_prod
* @property integer $gitlab_project_id
* @property Brand $brand_id
* @property int $id
* @property string $name
* @property string|null $url_re7
* @property string|null $url_prod
* @property int|null $gitlab_project_id
* @property int|null $brand_id Relation is nullable, since not all site would be affiliate with a brand.
* @property int|null $project_manager_id Project manager relationship
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Brand|null $brand
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read \App\Models\User|null $projectManager
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read mixed $value
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withoutTrashed()
* @mixin \Eloquent
*/
class Site extends Model
{
@ -57,6 +72,10 @@ class Site extends Model
static::updating(function (self $model) {
self::manageFields($model);
});
static::deleted(function ($site) {
$site->invoices()->delete();
$site->tickets()->delete();
});
}
// * METHODS
@ -84,7 +103,7 @@ class Site extends Model
* HasMany Message relation
* The users of type client that would have access to this site tickets
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
* @return \Illuminate\Database\Eloquent\Relations\belongsToMany
*/
public function users()
{
@ -101,6 +120,16 @@ class Site extends Model
return $this->hasMany(Ticket::class, 'site_id');
}
/**
* HasMany Invoices relation
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function invoices()
{
return $this->hasMany(Invoice::class, 'site_id');
}
/**
* BelongsTo Brand relation
*
@ -122,16 +151,6 @@ class Site extends Model
return $this->belongsTo(User::class, 'project_manager_id');
}
/**
* BelongsTo Project relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class);
}
/**
* Return value attribute for belongsto search component
*

View file

@ -4,6 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $site_id
* @property int $user_id
* @mixin \Eloquent
*/
class SiteUser extends Pivot
{
/**

View file

@ -2,26 +2,68 @@
namespace App\Models;
use App\Enums\Users\RoleEnum;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Jobs\SendTicketMentionMail;
use App\Mail\TicketMention;
use Illuminate\Support\Str;
use App\Models\GitlabIssues;
use App\Enums\Users\RoleEnum;
use App\Models\Credit\Account;
use App\Models\Credit\Estimation;
use App\Models\Ticket\TicketType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Jobs\SendTicketMentionMail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Tickets\TicketStatusEnum;
use Illuminate\Database\Eloquent\Model;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\ModelNotFoundException;
/**
* @property int $id
* @property int $created_by
* @property int|null $assigned_to La personne a qui est assigné le ticket.
* @property int $site_id
* @property TicketTypeEnum $type
* @property string $title Titre du ticket
* @property string $text
* @property string|null $file
* @property string|null $file2
* @property string|null $file3 Pièce jointes n°3
* @property string|null $file4 Pièce jointes n°4
* @property string|null $file5 Pièce jointes n°5
* @property TicketStatusEnum $status
* @property int $order Ticket order.
* @property bool $is_internal
* @property string|null $gitlab_issue URL de l'issue Gitlab
* @property \Illuminate\Support\Carbon|null $date_mep Date MEP souhaitée
* @property int $last_activity Last activity on the ticket.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\User|null $assignedUser
* @property-read Estimation|null $estimation
* @property-read string $full_name
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Label> $labels
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $linkedTickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read \App\Models\User|null $projectManager
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $subscriptions
* @property-read TicketType|null $ticketType
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withoutTrashed()
* @mixin \Eloquent
*/
class Ticket extends Model
{
use HasFactory;
use SoftDeletes;
/**
* The attributes that are fillable.
@ -61,8 +103,8 @@ class Ticket extends Model
protected static function booted()
{
static::creating(function (Ticket $ticket) {
// If bug force the ticket status on new. All the others are estimateTodo.
self::setOrder($ticket);
// If bug force the ticket status on new. All the others are estimateTodo.
if ($ticket->type->equals(TicketTypeEnum::bug)) {
$ticket->status = TicketStatusEnum::new;
} else {
@ -70,24 +112,23 @@ class Ticket extends Model
}
});
static::updating(function (Ticket $ticket) {
if (!$ticket->original['status'] !== $ticket->status->value && $ticket->original['order'] === $ticket->order) {
if (
!$ticket->original['status'] !== $ticket->status->value && $ticket->original['order'] === $ticket->order
) {
$user = auth()->user();
event(new TicketStatusChanged(
$ticket,
TicketStatusEnum::make($ticket->original['status']),
auth()->user(),
$user,
null,
false
));
event(new NotifySubscriptions($ticket, sprintf(
'<b>%s</b> a modifié le statut en <b>%s</b>',
auth()->user()->name,
$ticket->status->label()
)));
event(new NotifyAdmin($ticket, sprintf(
'<b>%s</b> a modifié le statut en <b>%s</b>',
auth()->user()->name,
$ticket->status->label()
)));
$message = $user ? sprintf('<b>%s</b> a modifié le statut en <b>%s</b>', $user->name, $ticket->status->label()) : sprintf('Le statut a été modifié en <b>%s</b>', $ticket->status->label());
event(new NotifySubscriptions($ticket, $message));
event(new NotifyAdmin($ticket, $message));
}
if ($ticket->status->equals(TicketStatusEnum::archived)) {
self::setOrder($ticket, true);
@ -104,7 +145,18 @@ class Ticket extends Model
*/
private static function setOrder(self $model, bool $isArchived = false): void
{
$model->order = \intval(self::query()->where('status', $isArchived ? '=' : '!=', TicketStatusEnum::archived)->max('order')) + 1;
$maxOrder = \intval(self::query()->where('status', '!=', TicketStatusEnum::archived)->max('order'));
if ($isArchived) {
$request = Ticket::query()
->where('site_id', $model->site_id)
->whereNot('status', TicketStatusEnum::archived)
->whereNot('id', $model->id)
->whereBetween('order', [$model->order, $maxOrder]);
$request->decrement('order', 1);
$model->order = 0;
return;
}
$model->order = $maxOrder + 1;
}
@ -152,6 +204,17 @@ class Ticket extends Model
return $this->belongsTo(User::class, 'assigned_to');
}
/**
* BelongsTo User relation
* The user that is in charge of the site linked to this ticket
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function projectManager()
{
return $this->belongsTo(User::class, 'project_manager_id');
}
/**
* HasMany Message relation
*

View file

@ -9,8 +9,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property TicketTypeEnum $type
* @property string $template
* @property int $id
* @property TicketTypeEnum $type TicketType Enum
* @property string $template Le template du type de ticket
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $tickets
* @mixin \Eloquent
*/
class TicketType extends Model
{

View file

@ -15,12 +15,27 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property RoleEnum $role
* @property Collection $sites
* @property string|null $remember_token
* @property int|null $gitlab_id ID du user Gitlab
* @property int|null $favourite_site_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Collection<int, \App\Models\Message> $messages
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read Collection<int, \App\Models\Site> $sites
* @property-read Collection<int, \App\Models\Ticket> $subscriptions
* @property-read Collection<int, \App\Models\Ticket> $tickets
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutTrashed()
* @mixin \Eloquent
*/
class User extends Authenticatable
{
@ -46,7 +61,8 @@ class User extends Authenticatable
'password',
'role',
'sites',
'gitlab_id'
'gitlab_id',
'favourite_site_id'
];
/**

View file

@ -0,0 +1,60 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
class ExportTicketsStateNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* The CSV content to be sent.
*
* @var string
*/
private string $csvContent;
/**
* Create a new notification instance.
*
* @param string $csvContent The content of the CSV file to be sent.
*/
public function __construct(string $csvContent)
{
$this->csvContent = $csvContent;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail(): \Illuminate\Notifications\Messages\MailMessage
{
return (new MailMessage())
->subject(trans("Export de l'état des lieux des tickets"))
->line(trans("Vous trouverez ci-joint le fichier CSV contenant l'état des tickets."))
->attachData(
$this->csvContent,
sprintf('tickets_state_%s.csv', Carbon::now()->format('Y-m-d_H-i-s')),
[
'mime' => 'text/csv',
]
);
}
}

View file

@ -7,7 +7,6 @@ use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Users\RoleEnum;
use App\Models\Ticket;
use App\Models\User;
use Error;
use Illuminate\Auth\Access\HandlesAuthorization;
class TicketPolicy
@ -284,7 +283,12 @@ class TicketPolicy
public function createGitlabIssue(User $user, Ticket $ticket)
{
return $user->role->equals(RoleEnum::developper)
&& ($ticket->type->equals(TicketTypeEnum::bug) || $ticket->type->equals(TicketTypeEnum::evolution) || $ticket->type->equals(TicketTypeEnum::newsletter))
&& (
$ticket->type->equals(TicketTypeEnum::bug)
|| $ticket->type->equals(TicketTypeEnum::evolution)
|| $ticket->type->equals(TicketTypeEnum::newsletter)
|| $ticket->type->equals(TicketTypeEnum::mailing)
)
&& !\is_null($ticket->site->gitlab_project_id)
&& \is_null($ticket->gitlab_issue);
}
@ -293,12 +297,11 @@ class TicketPolicy
* Determine if the user can changeOrder
*
* @param User $user
* @return bool
* @throws Error
* @return boolean
* @throws Error The error.
*/
public function changeOrder(User $user)
{
return $user->role->equals(RoleEnum::developper);
}
}

View file

@ -2,12 +2,24 @@
namespace App\Providers;
use App\Repositories\EloquentSiteRepository;
use App\Repositories\SiteRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Override;
class AppServiceProvider extends ServiceProvider
{
#[Override]
public function register()
{
$this->app->singleton(
SiteRepository::class,
fn() => new EloquentSiteRepository()
);
}
/**
* Bootstrap any application services.
*
@ -24,7 +36,7 @@ class AppServiceProvider extends ServiceProvider
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
request()->getClientIp();
}
});

View file

@ -2,14 +2,6 @@
namespace App\Providers;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\NotifyTicketCreator;
use App\Events\TicketStatusChanged;
use App\Listeners\AddStatusMessages;
use App\Listeners\SendNotificationMailToAdmin;
use App\Listeners\SendNotificationMailToTicketCreator;
use App\Listeners\SendSubscriptionsNotificationsMail;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -23,10 +15,6 @@ class EventServiceProvider extends ServiceProvider
*/
protected $listen = [
Registered::class => [SendEmailVerificationNotification::class],
TicketStatusChanged::class => [AddStatusMessages::class],
NotifySubscriptions::class => [SendSubscriptionsNotificationsMail::class],
NotifyTicketCreator::class => [SendNotificationMailToTicketCreator::class],
NotifyAdmin::class => [SendNotificationMailToAdmin::class],
];
/**
@ -36,6 +24,6 @@ class EventServiceProvider extends ServiceProvider
*/
public function shouldDiscoverEvents(): bool
{
return false;
return true;
}
}

View file

@ -10,7 +10,6 @@ use App\Models\Information;
use App\Models\Invoice;
use App\Models\Label;
use App\Models\Package;
use App\Models\Project;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\Ticket\TicketType;
@ -88,14 +87,12 @@ class RouteServiceProvider extends ServiceProvider
});
Route::model('brand', Brand::class);
Route::model('label', Label::class);
Route::model('marker', Marker::class);
Route::model('package', Package::class);
Route::model('information', Information::class);
Route::model('company', Company::class);
Route::model('invoice', Invoice::class);
Route::model('user', User::class);
Route::model('site', Site::class);
Route::model('project', Project::class);
Route::model('ticket_type', TicketType::class);
Route::bind('ticket', function ($value) {
return Ticket::where('id', $value)->has('site')->firstOrFail();

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
use Override;
class EloquentSiteRepository implements SiteRepository
{
#[Override]
public function findByName(string $name): ?Site
{
return Site::query()
->where('name', $name)
->first();
}
}

View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
interface SiteRepository
{
public function findByName(string $name): ?Site;
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
class AutomaticTicketCreator
{
public function __construct(
private readonly BrevoWebookParser $parser
) {}
public function createTicket(
Site $forSite,
User $withProjectManager,
string $withRawHtmlContent,
): Ticket {
$parsedContent = $this->parser->parseRawHtml($withRawHtmlContent);
$textContent = "Bonjour,\n";
$textContent .= " Contenu :\n\n";
$textContent .= " {$parsedContent->plainTextContent},\n\n";
$textContent .= " Cordialement.";
$title = "Demande de mail en masse";
if (!empty($parsedContent->brand)) {
$title .= " pour {$parsedContent->brand}";
}
$ticket = new Ticket([
'site_id' => $forSite->id,
'type' => TicketTypeEnum::mailing->value,
'status' => TicketStatusEnum::estimateTodo->value,
'date_mep' => null,
'text' => $textContent,
'title' => $title,
'is_internal' => false
]);
$ticket->site()->associate($forSite);
$ticket->user()->associate($withProjectManager);
$ticket->assignedUser()->associate($withProjectManager);
$ticket->saveOrFail();
return $ticket;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Dto\BrevoWebHookDto;
use App\Exceptions\BrevoParseFailure;
class BrevoWebookParser
{
/** @throws BrevoParseFailure */
public function parseRawHtml(string $rawHtml): BrevoWebHookDto
{
$data = [];
$plainText = strip_tags($rawHtml);
$tblRegexPatterns = [
'user' => '/^Matricule et Nom du demandeur\s*\r?\n(.*)$/im',
'email' => '/^Email du demandeur\s*\r?\n(.*)$/im',
'brand' => '/^Marque\s*\/\s*Filiale\s*\r?\n(.*)$/im',
];
foreach ($tblRegexPatterns as $field => $regex) {
if (preg_match($regex, $plainText, $matches)) {
$data[$field] = trim($matches[1]);
}
}
return new BrevoWebHookDto(
plainTextContent: $plainText,
brand: $data['brand'] ?? null
);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Traits\Controllers;
use App\Models\GitlabIssues;
use GrahamCampbell\GitLab\Facades\GitLab;
trait HasGitlabIssue
{
/**
* Retrieves a GitLab issue id from its URL.
*
* @param int $projectId
* @param string $issueUrl
*
* @return array
*/
public function getGitlabIssue(int $projectId, string $issueUrl): array
{
if (!preg_match('/\/(?:issues|work_items)\/(\d+)/', $issueUrl, $matches)) { return []; }
$gitlabIssue = GitLab::issues()->show($projectId, (int) $matches[1]);
return $gitlabIssue;
}
/**
* Update labels for a GitLab issue.
*
* @param int|string $projectId
* @param int $issueId
* @param array $labels
*
* @return array
*/
public function updateGitlabIssueLabels($projectId, int $issueId, array $labels): array
{
return GitLab::issues()->update($projectId, $issueId, [
'labels' => implode(',', $labels),
]);
}
/**
* Close a GitLab issue.
*
* @param int|string $projectId
* @param int $issueId
* @param array $labels
*
* @return array
*/
public function closeGitlabIssue($projectId, int $issueId, array $labels): array
{
return GitLab::issues()->update($projectId, $issueId, [
'state_event' => 'close',
'labels' => implode(',', $labels),
]);
}
}

View file

@ -4,7 +4,7 @@
"type": "project",
"description": "Support web",
"support": {
"email": "equipe.web@ldc.fr"
"email": "digital@ldc.fr"
},
"keywords": [
"framework",
@ -12,22 +12,23 @@
],
"license": "MIT",
"require": {
"php": "^8.3",
"php": "^8.5",
"carlos-meneses/laravel-mpdf": "^2.1",
"codegreencreative/laravel-samlidp": "^5.2",
"diglactic/laravel-breadcrumbs": "^9.0",
"doctrine/dbal": "^3.1",
"graham-campbell/gitlab": "^7.0",
"graham-campbell/gitlab": "^7.6",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"mews/purifier": "^3.4",
"sentry/sentry-laravel": "*",
"uwil/admin-secure-laravel": "^3.0",
"uwil/admin-secure-laravel": "^5.0",
"uwil/k8s-laravel": "^1.2"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.7",
"fakerphp/faker": "^1.9.1",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0",
@ -43,7 +44,8 @@
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
"Database\\Seeders\\": "database/seeders/",
"Domain\\": "domain/"
}
},
"repositories": [
@ -67,7 +69,11 @@
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"@php artisan lang:update",
"@php artisan ide-helper:generate",
"@php artisan ide-helper:models -RW"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

2960
composer.lock generated

File diff suppressed because it is too large Load diff

379
config/ide-helper.php Normal file
View file

@ -0,0 +1,379 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Session\Store;
return [
/*
|--------------------------------------------------------------------------
| Filename
|--------------------------------------------------------------------------
|
| The default filename.
|
*/
'filename' => '_ide_helper.php',
/*
|--------------------------------------------------------------------------
| Models filename
|--------------------------------------------------------------------------
|
| The default filename for the models helper file.
|
*/
'models_filename' => '_ide_helper_models.php',
/*
|--------------------------------------------------------------------------
| PhpStorm meta filename
|--------------------------------------------------------------------------
|
| PhpStorm also supports the directory `.phpstorm.meta.php/` with arbitrary
| files in it, should you need additional files for your project; e.g.
| `.phpstorm.meta.php/laravel_ide_Helper.php'.
|
*/
'meta_filename' => '.phpstorm.meta.php',
/*
|--------------------------------------------------------------------------
| Fluent helpers
|--------------------------------------------------------------------------
|
| Set to true to generate commonly used Fluent methods.
|
*/
'include_fluent' => false,
/*
|--------------------------------------------------------------------------
| Write model query methods
|--------------------------------------------------------------------------
|
| Set to false to disable generated docs for the 'query()', 'newQuery()' and 'newModelQuery()' methods.
|
*/
'write_query_methods' => false,
/*
|--------------------------------------------------------------------------
| Write model magic methods
|--------------------------------------------------------------------------
|
| Set to false to disable write magic methods of model.
|
*/
'write_model_magic_where' => false,
/*
|--------------------------------------------------------------------------
| Write model external Eloquent builder methods
|--------------------------------------------------------------------------
|
| Set to false to disable write external Eloquent builder methods.
|
*/
'write_model_external_builder_methods' => false,
/*
|--------------------------------------------------------------------------
| Write model relation count and exists properties
|--------------------------------------------------------------------------
|
| Set to false to disable writing of relation count and exists properties
| to model DocBlocks.
|
*/
'write_model_relation_count_properties' => false,
'write_model_relation_exists_properties' => false,
/*
|--------------------------------------------------------------------------
| Write Eloquent model mixins
|--------------------------------------------------------------------------
|
| This will add the necessary DocBlock mixins to the model class
| contained in the Laravel framework. This helps the IDE with
| auto-completion.
|
| Please be aware that this setting changes a file within the /vendor directory.
|
*/
'write_eloquent_model_mixins' => false,
/*
|--------------------------------------------------------------------------
| Helper files to include
|--------------------------------------------------------------------------
|
| Include helper files. By default not included, but can be toggled with the
| -- helpers (-H) option. Extra helper files can be included.
|
*/
'include_helpers' => false,
'helper_files' => [
base_path() . '/vendor/laravel/framework/src/Illuminate/Support/helpers.php',
base_path() . '/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php',
],
/*
|--------------------------------------------------------------------------
| Model locations to include
|--------------------------------------------------------------------------
|
| Define in which directories the ide-helper:models command should look
| for models.
|
| glob patterns are supported to easier reach models in sub-directories,
| e.g. `app/Services/* /Models` (without the space).
|
*/
'model_locations' => [
'app',
],
/*
|--------------------------------------------------------------------------
| Models to ignore
|--------------------------------------------------------------------------
|
| Define which models should be ignored.
|
*/
'ignored_models' => [],
/*
|--------------------------------------------------------------------------
| Models hooks
|--------------------------------------------------------------------------
|
| Define which hook classes you want to run for models to add custom information.
|
| Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface.
|
*/
'model_hooks' => [
// App\Support\IdeHelper\MyModelHook::class
],
/*
|--------------------------------------------------------------------------
| Extra classes
|--------------------------------------------------------------------------
|
| These implementations are not really extended, but called with magic functions.
|
*/
'extra' => [
'Eloquent' => [Builder::class, \Illuminate\Database\Query\Builder::class],
'Session' => [Store::class],
],
'magic' => [],
/*
|--------------------------------------------------------------------------
| Interface implementations
|--------------------------------------------------------------------------
|
| These interfaces will be replaced with the implementing class. Some interfaces
| are detected by the helpers, others can be listed below.
|
*/
'interfaces' => [
// App\MyInterface::class => App\MyImplementation::class,
],
/*
|--------------------------------------------------------------------------
| Support for camel cased models
|--------------------------------------------------------------------------
|
| There are some Laravel packages (such as Eloquence) that allow for accessing
| Eloquent model properties via camel case, instead of snake case.
|
| Enabling this option will support these packages by saving all model
| properties as camel case, instead of snake case.
|
| For example, normally you would see this:
|
| * @property \Illuminate\Support\Carbon $created_at
| * @property \Illuminate\Support\Carbon $updated_at
|
| With this enabled, the properties will be this:
|
| * @property \Illuminate\Support\Carbon $createdAt
| * @property \Illuminate\Support\Carbon $updatedAt
|
| Note, it is currently an all-or-nothing option.
|
*/
'model_camel_case_properties' => false,
/*
|--------------------------------------------------------------------------
| Property casts
|--------------------------------------------------------------------------
|
| Cast the given "real type" to the given "type".
|
*/
'type_overrides' => [
'integer' => 'int',
'boolean' => 'bool',
],
/*
|--------------------------------------------------------------------------
| Include DocBlocks from classes
|--------------------------------------------------------------------------
|
| Include DocBlocks from classes to allow additional code inspection for
| magic methods and properties.
|
*/
'include_class_docblocks' => false,
/*
|--------------------------------------------------------------------------
| Force FQN usage
|--------------------------------------------------------------------------
|
| Use the fully qualified (class) name in DocBlocks,
| even if the class exists in the same namespace
| or there is an import (use className) of the class.
|
*/
'force_fqn' => false,
/*
|--------------------------------------------------------------------------
| Use generics syntax
|--------------------------------------------------------------------------
|
| Use generics syntax within DocBlocks,
| e.g. `Collection<User>` instead of `Collection|User[]`.
|
*/
'use_generics_annotations' => true,
/*
|--------------------------------------------------------------------------
| Default return types for macros
|--------------------------------------------------------------------------
|
| Define default return types for macros without explicit return types.
| e.g. `\Illuminate\Database\Query\Builder::class => 'static'`,
| `\Illuminate\Support\Str::class => 'string'`
|
*/
'macro_default_return_types' => [
Factory::class => PendingRequest::class,
],
/*
|--------------------------------------------------------------------------
| Additional relation types
|--------------------------------------------------------------------------
|
| Sometimes it's needed to create custom relation types. The key of the array
| is the relationship method name. The value of the array is the fully-qualified
| class name of the relationship, e.g. `'relationName' => RelationShipClass::class`.
|
*/
'additional_relation_types' => [],
/*
|--------------------------------------------------------------------------
| Additional relation return types
|--------------------------------------------------------------------------
|
| When using custom relation types its possible for the class name to not contain
| the proper return type of the relation. The key of the array is the relationship
| method name. The value of the array is the return type of the relation ('many'
| or 'morphTo').
| e.g. `'relationName' => 'many'`.
|
*/
'additional_relation_return_types' => [],
/*
|--------------------------------------------------------------------------
| Enforce nullable Eloquent relationships on not null columns
|--------------------------------------------------------------------------
|
| When set to true (default), this option enforces nullable Eloquent relationships.
| However, in cases where the application logic ensures the presence of related
| records it may be desirable to set this option to false to avoid unwanted null warnings.
|
| Default: true
| A not null column with no foreign key constraint will have a "nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation|null $notNullColumnWithNoForeignKeyConstraint
|
| Option: false
| A not null column with no foreign key constraint will have a "not nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
|
*/
'enforce_nullable_relationships' => true,
/*
|--------------------------------------------------------------------------
| Make soft deletable relations nullable
|--------------------------------------------------------------------------
|
| When set to true (default), relationships to models using SoftDeletes trait
| will be marked as nullable. This is because soft-deleted records are excluded
| from queries by default, meaning even non-nullable foreign keys can return
| null when the related model is soft-deleted.
|
| Default: true
| A relationship to a soft-deletable model will include |null in the type:
| * @property-read Team|null $team
|
| Option: false
| A relationship to a soft-deletable model will NOT include |null (unless
| nullable for other reasons such as nullable foreign key column):
| * @property-read Team $team
|
*/
'soft_deletes_force_nullable' => true,
/*
|--------------------------------------------------------------------------
| Run artisan commands after migrations to generate model helpers
|--------------------------------------------------------------------------
|
| The specified commands should run after migrations are finished running.
|
*/
'post_migrate' => [
// 'ide-helper:models --nowrite',
],
];

View file

@ -16,7 +16,11 @@ return [
'default' => env('MAIL_MAILER', 'smtp'),
// * Mail admin
'MAIL_ADMIN' => env('MAIL_ADMIN', 'equipe.web@ldc.fr'),
'MAIL_ADMIN' => env('MAIL_ADMIN', 'digital@ldc.fr'),
'brevo_webook' => [
'expected_from_address' => 'mailing@reply.uwil.fr'
],
/*
|--------------------------------------------------------------------------
@ -100,7 +104,7 @@ return [
],
'contactsList' => [
'serviceweb' => env('MAIL_LIST_SERVICEWEB', 'equipe.web@ldc.fr')
'serviceweb' => env('MAIL_LIST_SERVICEWEB', 'digital@ldc.fr')
],
/*

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Site;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class SiteFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Site::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->company(),
'url_re7' => fake()->url(),
'url_prod' => fake()->url(),
'gitlab_project_id' => fake()->optional()->randomNumber(4),
'brand_id' => null,
'project_manager_id' => null,
];
}
/**
* Indique que le site a un chef de projet assigné.
*/
public function withProjectManager(): static
{
return $this->state(fn(array $attributes) => [
'project_manager_id' => User::factory(),
]);
}
}

View file

@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\User;
use App\Enums\Users\RoleEnum;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
@ -18,17 +20,23 @@ class UserFactory extends Factory
/**
* Define the model's default state.
*
* @return array
* @return array<string, mixed>
*/
public function definition()
public function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
'email_verified_at' => now(),
// * password
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'role' => RoleEnum::developper,
'gitlab_id' => fake()->optional()->randomNumber(5),
'favourite_site_id' => null,
];
}
public function admin(): static
{
return $this->state(fn(array $attributes) => [
'role' => RoleEnum::admin,
]);
}
}

View file

@ -16,12 +16,12 @@ class CreateCreditLogsTable extends Migration
Schema::create('credit_logs', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('type')->comment('Enum Credit\\BalanceOperationEnum');
$table->unsignedFloat('amount')->comment('Montant de l\'opération');
$table->tinyInteger('type')->unsigned()->comment('Enum Credit\\BalanceOperationEnum');
$table->float('amount')->unsigned()->comment('Montant de l\'opération');
$table->foreignId('credit_account_id')
->constrained('credit_accounts')
->cascadeOnDelete();
->constrained('credit_accounts')
->cascadeOnDelete();
$table->timestamp('created_at');
});

View file

@ -18,24 +18,24 @@ class CreateCreditEstimationsTable extends Migration
Schema::create('credit_estimations', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('status')->default(EstimationStatusEnum::pending)
->comment('Enum Credit\\EstimationStatusEnum');
$table->tinyInteger('status')->unsigned()->default(EstimationStatusEnum::pending)
->comment('Enum Credit\\EstimationStatusEnum');
$table->unsignedFloat('amount')->comment('Estimation amount');
$table->float('amount')->unsigned()->comment('Estimation amount');
$table->foreignIdFor(Ticket::class)
->constrained();
$table->foreignId('credit_account_id')
->comment('The account to debit.')
->constrained('credit_accounts');
->comment('The account to debit.')
->constrained('credit_accounts');
$table->foreignId('estimator_id')
->comment('The person who has done the estimate.')
->constrained('users');
->comment('The person who has done the estimate.')
->constrained('users');
$table->foreignId('validator_id')
->nullable()
->comment('The person who has validate the estimate.')
->constrained('users');
->nullable()
->comment('The person who has validate the estimate.')
->constrained('users');
$table->timestamps();
});

View file

@ -1,6 +1,7 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
@ -12,8 +13,8 @@ class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
*/
public function up()
{
Schema::table('credit_estimations', function ($table) {
$table->unsignedFloat('amount', 8, 3)
Schema::table('credit_estimations', function (Blueprint $table) {
$table->decimal('amount', 8, 3)->unsigned()
->comment('Estimation amount')->change();
});
}
@ -25,8 +26,8 @@ class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
*/
public function down()
{
Schema::table('credit_estimations', function ($table) {
$table->unsignedFloat('amount')
Schema::table('credit_estimations', function (Blueprint $table) {
$table->float('amount')->unsigned()
->comment('Estimation amount')->change();
});
}

View file

@ -17,7 +17,7 @@ class CreatePackagesTable extends Migration
$table->id();
$table->string('name');
$table->unsignedFloat('amount')->comment('The cost per day');
$table->float('amount')->unsigned()->comment('The cost per day');
});
}

View file

@ -37,8 +37,8 @@ class CreateInvoicesTable extends Migration
'A JSON object, each line has to be contructed using these fields description, days, type Invoices\TaskTypeEnum, amount'
);
$table->unsignedFloat('total')->comment('The total calculated using all tasks');
$table->unsignedFloat('total_days')->comment('The total days calculated using all tasks');
$table->float('total')->unsigned()->comment('The total calculated using all tasks');
$table->float('total_days')->unsigned()->comment('The total days calculated using all tasks');
$table->text('comment')->nullable()
->comment('Any comment joined to the invoice');

View file

@ -1,6 +1,6 @@
<?php
use App\Models\Ticket;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
class ChangeTicketsStatus extends Migration
@ -12,17 +12,22 @@ class ChangeTicketsStatus extends Migration
*/
public function up()
{
Ticket::whereIn('status', [4, 5, 7, 10])
DB::table('tickets')->whereIn('status', [4, 5, 7, 10])
->update(['status' => 3]);
Ticket::where('status', 9)
DB::table('tickets')->where('status', 9)
->update(['status' => 4]);
Ticket::where('status', 17)
DB::table('tickets')->where('status', 17)
->update(['status' => 5]);
Ticket::where('status', 12)
DB::table('tickets')->where('status', 12)
->update(['status' => 6]);
Ticket::where('status', 13)
DB::table('tickets')->where('status', 13)
->update(['status' => 7]);
Ticket::where('status', 15)
DB::table('tickets')->where('status', 15)
->update(['status' => 8]);
}
@ -33,17 +38,22 @@ class ChangeTicketsStatus extends Migration
*/
public function down()
{
Ticket::where('status', 3)
DB::table('tickets')->where('status', 3)
->update(['status' => 10]);
Ticket::where('status', 4)
DB::table('tickets')->where('status', 4)
->update(['status' => 9]);
Ticket::where('status', 5)
DB::table('tickets')->where('status', 5)
->update(['status' => 17]);
Ticket::where('status', 6)
DB::table('tickets')->where('status', 6)
->update(['status' => 12]);
Ticket::where('status', 7)
DB::table('tickets')->where('status', 7)
->update(['status' => 13]);
Ticket::where('status', 8)
DB::table('tickets')->where('status', 8)
->update(['status' => 15]);
}
}

View file

@ -1,9 +1,8 @@
<?php
use App\Models\Ticket;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@ -17,15 +16,32 @@ return new class extends Migration
{
Schema::table('tickets', function (Blueprint $table) {
$table->timestamp('last_activity')
->default(Carbon::now())
->useCurrent()
->comment('Last activity on the ticket.')
->after('is_internal');
});
Ticket::get()->map(function (object $ticket) {
$lastMessage = $ticket->messages->whereNotNull('user_id')->sortByDesc('updated_at')->first();
$ticket->last_activity =
$lastMessage->updated_at ?? $ticket->updated_at;
$ticket->saveQuietly();
DB::table('tickets')->orderBy('id')->chunk(500, function ($tickets) {
$ticketIds = $tickets->pluck('id')->toArray();
$messages = DB::table('messages')
->whereIn('ticket_id', $ticketIds)
->whereNotNull('user_id')
->orderBy('updated_at', 'desc')
->get()
->groupBy('ticket_id');
foreach ($tickets as $ticket) {
$lastMessageDate = $messages->has($ticket->id)
? $messages->get($ticket->id)->first()->updated_at
: null;
DB::table('tickets')
->where('id', $ticket->id)
->update([
'last_activity' => $lastMessageDate ?? $ticket->updated_at
]);
}
});
}

View file

@ -1,10 +1,9 @@
<?php
use App\Models\Ticket;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
@ -22,11 +21,15 @@ return new class extends Migration
});
DB::transaction(function () {
$tickets = Ticket::all();
foreach ($tickets as $ticket) {
$ticket->title = 'ts-' . $ticket->id;
$ticket->saveQuietly();
}
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
foreach ($tickets as $ticket) {
DB::table('tickets')
->where('id', $ticket->id)
->update([
'title' => 'ts-' . $ticket->id
]);
}
});
});
}

View file

@ -1,8 +1,7 @@
<?php
use App\Models\Ticket;
use App\Models\Ticket\TicketType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
@ -13,13 +12,19 @@ return new class extends Migration
*/
public function up(): void
{
Ticket::query()->get()->map(function (Ticket $model) {
$model->type = $model->getRawOriginal('type') + 1;
$model->saveQuietly();
});
TicketType::query()->orderBy('type', 'DESC')->get()->map(function (TicketType $model) {
$model->type = $model->getRawOriginal('type') + 1;
$model->saveQuietly();
DB::transaction(function () {
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
$ids = $tickets->pluck('id')->toArray();
DB::table('tickets')->whereIn('id', $ids)->increment('type');
});
$ticketTypes = DB::table('ticket_types')->orderBy('type', 'desc')->get();
foreach ($ticketTypes as $type) {
DB::table('ticket_types')
->where('id', $type->id)
->update(['type' => $type->type + 1]);
}
});
}
@ -30,13 +35,20 @@ return new class extends Migration
*/
public function down(): void
{
Ticket::query()->get()->map(function (Ticket $model) {
$model->type = $model->getRawOriginal('type') - 1;
$model->saveQuietly();
});
TicketType::query()->orderBy('type', 'DESC')->get()->map(function (TicketType $model) {
$model->type = $model->getRawOriginal('type') - 1;
$model->saveQuietly();
DB::transaction(function () {
// 1. Restauration des tickets
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
$ids = $tickets->pluck('id')->toArray();
DB::table('tickets')->whereIn('id', $ids)->decrement('type');
});
$ticketTypes = DB::table('ticket_types')->orderBy('type', 'asc')->get();
foreach ($ticketTypes as $type) {
DB::table('ticket_types')
->where('id', $type->id)
->update(['type' => $type->type - 1]);
}
});
}
};

View file

@ -1,7 +1,7 @@
<?php
use App\Models\Ticket;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
@ -12,10 +12,9 @@ return new class extends Migration
*/
public function up(): void
{
Ticket::query()->where('status', '>', 6)->get()->map(function (Ticket $model) {
$model->status = $model->getRawOriginal('status') + 1;
$model->saveQuietly();
});
DB::table('tickets')
->where('status', '>', 6)
->increment('status');
}
/**
@ -25,9 +24,8 @@ return new class extends Migration
*/
public function down(): void
{
Ticket::query()->where('status', '>', 6)->get()->map(function (Ticket $model) {
$model->status = $model->getRawOriginal('status') - 1;
$model->saveQuietly();
});
DB::table('tickets')
->where('status', '>', 6)
->decrement('status');
}
};

View file

@ -1,9 +1,9 @@
<?php
use Illuminate\Support\Facades\Schema;
use App\Models\Site;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
@ -15,14 +15,31 @@ return new class extends Migration
public function up(): void
{
Schema::table('tickets', function (Blueprint $table) {
$table->integer('order')->after('status')->comment('Ticket order.');
$table->integer('order')->default(0)->after('status')->comment('Ticket order.');
});
Site::query()->get()->map(function ($site) {
$site->tickets->map(function ($ticket, $ticketIndex) {
$ticket->order = $ticketIndex + 1;
$ticket->saveQuietly();
});
DB::transaction(function () {
$currentSiteId = null;
$orderCounter = 1;
DB::table('tickets')
->orderBy('site_id')
->orderBy('id')
->chunk(1000, function ($tickets) use (&$currentSiteId, &$orderCounter) {
foreach ($tickets as $ticket) {
if ($ticket->site_id !== $currentSiteId) {
$currentSiteId = $ticket->site_id;
$orderCounter = 1;
}
DB::table('tickets')
->where('id', $ticket->id)
->update(['order' => $orderCounter]);
$orderCounter++;
}
});
});
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropColumn('deleted_at');
});
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('tickets', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('tickets', function (Blueprint $table) {
$table->dropColumn('deleted_at');
});
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('favourite_site_id')
->nullable()
->after('gitlab_id')
->constrained('sites')
->onDelete('set null');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['favourite_site_id']);
$table->dropColumn('favourite_site_id');
});
}
};

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