diff --git a/mini-crm/REPERES_TP3_TP4_TP5.md b/mini-crm/REPERES_TP3_TP4_TP5.md new file mode 100644 index 0000000..7e03635 --- /dev/null +++ b/mini-crm/REPERES_TP3_TP4_TP5.md @@ -0,0 +1,70 @@ +# Repères des modifications : TP3, TP4 et TP5 + +Les commentaires `TP3`, `TP4` et `TP5` dans le code indiquent **à quel exercice sert chaque partie**. Ils n'ont aucun effet sur le fonctionnement de Laravel. Certains fichiers du TP3 existaient déjà avant les corrections ; le repère identifie la fonctionnalité, pas l'auteur de chaque ligne. + +Le TP6 a maintenant son propre guide : [`REPERES_TP6.md`](REPERES_TP6.md). Les règles du TP5 ont été déplacées des contrôleurs vers les Form Requests au TP6 ; leur comportement reste le même. + +## TP3 - Base de données et Eloquent + +Objectif : enregistrer des clients et des produits dans SQLite, puis les lire avec Eloquent et les afficher dans Blade. + +| Fichier | Rôle | +| --- | --- | +| `database/migrations/2026_09_16_075551_create_clients_table.php` | Crée la table `clients` initiale. | +| `database/migrations/2026_09_16_085026_create_product_table.php` | Crée la table `products` au pluriel, selon la convention du modèle `Product`. | +| `app/Models/Client.php`, `app/Models/Product.php` | Déclarent les champs `$fillable` pour `create()` et `update()`. Le téléphone a été ajouté au modèle Client au TP5. | +| `database/seeders/ClientSeeder.php`, `database/seeders/ProductSeeder.php` | Contiennent les trois clients et les cinq produits d'exemple. | +| `database/seeders/DatabaseSeeder.php` | Appelle les deux seeders. | +| `app/Http/Controllers/ProductController.php` (`index`) | Lit les produits avec `Product::all()` et transmet les résultats à la vue. | +| `resources/views/products/index.blade.php` | Affiche les produits. La liste a ensuite reçu les actions CRUD du TP4. | + +La liste clients du TP3 est maintenant assurée par `ClientController@index` : `Client::all()` lit la table et `clients.index` affiche les données. L'ancien `ClientsController.php` reste dans le projet, mais les routes utilisent `ClientController.php` depuis le TP4. + +## TP4 - CRUD complet + +Objectif : créer, lister, voir, modifier et supprimer un client ou un produit. + +| Fichier | Rôle | +| --- | --- | +| `routes/web.php` | Les deux appels `Route::resource()` génèrent sept routes pour Clients et sept pour Produits. | +| `app/Http/Controllers/ClientController.php` | Méthodes `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`. | +| `app/Http/Controllers/ProductController.php` | Mêmes actions pour les produits. `index` reprend la lecture du TP3. | +| `resources/views/clients/index.blade.php`, `resources/views/products/index.blade.php` | Listes, totaux et actions Voir/Modifier/Supprimer ; indication du stock faible pour les produits. | +| `resources/views/clients/create.blade.php`, `resources/views/clients/edit.blade.php` | Formulaires de création et modification d'un client. | +| `resources/views/products/create.blade.php`, `resources/views/products/edit.blade.php` | Formulaires équivalents pour les produits. | +| `resources/views/clients/show.blade.php`, `resources/views/products/show.blade.php` | Détail d'un seul enregistrement. | +| `resources/views/layouts/app.blade.php` | Navigation entre les pages. | +| `tests/Feature/CrudTest.php` | Vérifie les opérations CRUD dans une base de test. | + +Dans les formulaires, `@csrf` protège les requêtes POST et `@method('PUT')` permet la mise à jour. La suppression utilise `@method('DELETE')` dans les listes. + +## TP5 - Validation des données + +Objectif : refuser les données incorrectes avant l'enregistrement, expliquer les erreurs à l'utilisateur et conserver les valeurs saisies. + +| Fichier | Rôle | +| --- | --- | +| `database/migrations/2026_10_07_000000_adapt_clients_and_products_for_tp5.php` | Ajoute `phone`, rend `city` facultative et autorise les prix décimaux. | +| `app/Models/Client.php` | Ajoute `phone` à `$fillable`. | +| `app/Http/Requests/StoreClientRequest.php`, `app/Http/Requests/UpdateClientRequest.php` | Règles TP5 pour le nom, l'email, le téléphone et la ville, déplacées ici au TP6 avec leurs messages français. | +| `app/Http/Requests/StoreProductRequest.php`, `app/Http/Requests/UpdateProductRequest.php` | Règles TP5 pour le nom, le prix et le stock, déplacées ici au TP6. | +| `resources/views/clients/create.blade.php`, `resources/views/clients/edit.blade.php` | Liste des erreurs, message sous chaque champ et anciennes valeurs avec `old()`. | +| `resources/views/products/create.blade.php`, `resources/views/products/edit.blade.php` | Même traitement pour les produits. | +| `resources/views/clients/index.blade.php`, `resources/views/clients/show.blade.php` | Affichent le téléphone et gèrent les champs facultatifs vides. | +| `tests/Feature/ValidationTp5Test.php` | Vérifie les données refusées, les messages, les anciennes valeurs, les mises à jour et un prix décimal. | + +Le sujet impose `phone: nullable|string|max:20` : cette règle accepte les lettres. Si le professeur demande un numéro composé uniquement de chiffres et séparateurs, il faudra ajouter une règle spécifique ; le TP5 ne la fournit pas. + +## Vérifier et lancer le projet + +Dans un terminal ouvert dans le dossier `mini-crm` : + +```powershell +php artisan migrate:status +php artisan test +php artisan serve +``` + +Puis ouvrir `http://127.0.0.1:8000/clients` ou `http://127.0.0.1:8000/products`. + +Les PDF `TP3_walkthrough.pdf`, `TP4_walkthrough_complet.pdf` et `TP5_walkthrough_complet.pdf` détaillent chaque TP. Les commentaires du code servent de repères rapides ; ce fichier indique où chercher. diff --git a/mini-crm/REPERES_TP6.md b/mini-crm/REPERES_TP6.md new file mode 100644 index 0000000..0a52e47 --- /dev/null +++ b/mini-crm/REPERES_TP6.md @@ -0,0 +1,37 @@ +# Repères TP6 dans le code + +Le TP6 a déplacé la validation hors des contrôleurs et ajouté la recherche et la pagination. Les commentaires `TP6` indiquent les lignes importantes directement dans le projet. + +| Fichier | Ce qui a été fait | +| --- | --- | +| `app/Http/Requests/StoreClientRequest.php` | Autorise la création, définit les règles et les messages clients du TP5. | +| `app/Http/Requests/UpdateClientRequest.php` | Valide la modification et ignore l'email du client courant dans le contrôle d'unicité. | +| `app/Http/Requests/StoreProductRequest.php` | Valide la création d'un produit. | +| `app/Http/Requests/UpdateProductRequest.php` | Valide la modification d'un produit. | +| `app/Http/Controllers/ClientController.php` | Reçoit les Form Requests avec injection de dépendances ; `index()` cherche dans nom, email et ville, puis pagine par 10. | +| `app/Http/Controllers/ProductController.php` | Même principe pour les produits ; recherche par nom et, pour le bonus, par prix. | +| `app/Http/Controllers/Controller.php` | L'ancien `index()` vide a été retiré pour laisser chaque contrôleur définir sa propre signature. | +| `resources/views/clients/index.blade.php` | Formulaire GET, total des résultats, liens de pagination. | +| `resources/views/products/index.blade.php` | Même chose pour les produits. | +| `resources/views/layouts/app.blade.php` | Taille lisible des flèches de pagination. | +| `tests/Feature/Tp6SearchPaginationTest.php` | Tests de recherche, pagination, conservation du filtre et validation Form Request. | + +## À retenir + +- `authorize()` retourne `true` dans ce TP : aucune restriction utilisateur n'est encore demandée. +- `rules()` décrit les données admises. `messages()` conserve les messages français du TP5. +- Laravel construit automatiquement le Form Request déclaré comme paramètre de `store()` ou `update()` et le valide avant d'exécuter la méthode. +- `$request->validated()` transmet uniquement les données acceptées à Eloquent. +- `?search=...` est un paramètre d'URL ; le formulaire utilise `GET` car la recherche ne modifie aucune donnée. +- `paginate(10)` limite chaque page à 10 éléments ; `links()` affiche la navigation ; `withQueryString()` garde `search` dans les liens suivants. +- Le total affiché utilise `total()`, car `count()` ne compte que les éléments de la page actuelle. +- Un `orderBy('id')` stabilise l'ordre des pages. + +Pour tester plus de dix clients ou produits, les tests automatisés créent des enregistrements dans une base SQLite temporaire. Ils ne remplissent pas la base personnelle avec des données d'essai. + +```powershell +php artisan test +php artisan serve +``` + +Ouvrir ensuite `/clients?search=jean` et `/products?search=clavier` dans le navigateur. Le déroulé détaillé et toutes les réponses sont dans `TP6_walkthrough_complet.pdf` et `TP6_rempli.pdf`. diff --git a/mini-crm/TP3_walkthrough.pdf b/mini-crm/TP3_walkthrough.pdf new file mode 100644 index 0000000..f04e8a2 --- /dev/null +++ b/mini-crm/TP3_walkthrough.pdf @@ -0,0 +1,112 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/PageMode /UseNone /Pages 9 0 R /Type /Catalog +>> +endobj +8 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261007002713+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261007002713+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +9 0 obj +<< +/Count 3 /Kids [ 4 0 R 5 0 R 6 0 R ] /Type /Pages +>> +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1130 +>> +stream +Gat=*gN&c;&:O:Sm%`=?'dQ%r4uQ+sdrD7q0[jY#.0]o:NVle_[mKgY=!b#4gJ.PIJ!')UQ*%BosQ6lOJ*@^-g=QMJMXg#N7ef;PqipA^HqHN?:sL5(k>DJ=]UX0sR@C;QTG,MZEnNB4(ce;7tH0`mB)8cKN\734Z(O#A[Q:6\jt-_nLY,B:FA!K5l'OF$4b[en]Aj4Cd6/q9!pu]@IT%>[OXMCFE+3mIZ#[JVWS,IOd;PI=+>VhQF6,.r@iAOQ-)$BtCV#mGRgk\5A@FT9E7s<\P!J5hi:rQ,`0ll`+#[Kb=.aV1or;3\ALF0kSf\?u%]=+8r(V9on9Pm#<`REQsj\>@AR%pLKd.%1T;0;$WpD%@ghWMU&XO2pY4IfUHoEg]-8q#UY-o$&FasU/0bKK>^Y_]a'$u,-dpu5S\,LR"[5%M8VV`9)mR%KYWe=Ui:Mi\5W`:!eX=ZQmhnd8WNJD3#pTmU/nmd^=4*RI8p:B5_Dkm@cW@d=5(iH^&pKPZ&g%JrS\)_B59Q;J?p5$>/4jO+0<kb)t_r5\k=ZBRT"AgSG(*%T%2V3cd8)=e';Zi&m!B]SbJ>9rr\K!q1*6*QmE7r!6p(r6sMQ[~>endstream +endobj +11 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1289 +>> +stream +Gatm:9lo;R&A@7.oO/5?$O$#JjHd$<4Y3RAB"2XlMBg=O7DJ!:'a4Bq6'/u>'eH'_u3sIbanOXWC&@+ci`jmcXS9egC8/oWoZ@)B-G.D4O-nTI`0/jhsQA]q[rcj@H!BNP&EI9EMS"*Gk;J>B5cqh5d^R!)f_k^bi%DH>Wc/'@-lM"=m^6C4\`.3rY$aZp]&$H/l9Lh0VP=?oQ0ok$7nJ0IWZ&u\Dq9SNGFpD)*tR3ftEeBe8jofg%7*-Y=%E#+'.cAoJ6M8nnJXbH[ab?[/Ffn^oS[E%X//_@I'$?NuK8P$+)1Qt5I@m!>_]t$5.[tE1[!IMV.38!-dd4,^rOkRXl[`$s5JqeqIG]f^i-Z6DVg!u3C)+*qLSMeT4h]pd;/rRNMNfArh:Bh(p(r/0Gj\),RD6qb\V*`]X.43(b=\Vo>*Z+PZ?H>!ibhdL\d=3gIaN4\:tGkgFKB'%u;`:8/X^)mF$-a>/f7"-8H=>e4'frERJ;\Xuendstream +endobj +12 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1224 +>> +stream +Gatm:gN)%,&:N/3m%^#S+TMX`l;Nj;dci_/a5QY2TiO'6]EKG6I!ukC8PpPPNSTUa(LW*+HZg3<-%OH;K[6q.GZV]CUAuI2fRm!o6#d&MMUdd&-7OmQ`K.]R:u:*lUEi<[=6?8b.gCKs21&9ehR&7Eh!_N7#=NbK`A3HI1l.;f]eh#Dgb<**'^8ubRRtXEHlRB"0AthNPWNg\`>]2&kcQsi_H"!jI%ikLE'\EAVPZPXBlJ;[l(7U*1P7?qFEpa<@+dBnWRfRV,?b>:MP5hFLVp_3P7Pi;UqiCj3:&Ra#T6;aqcC$&"o3DE^UpDaHI9X3['_IACsC,0;^/35,uXcN"V9!6f?&ki[k,EVU]fN=OJ74e>?o-F$=Y"NWDQ4,(S-k;(RNWQ].tb!;W0K9`7C)H@QK\T2=dL,cAIcLhqJ0Uk]uE#94gGJup]!)iTDmGd,3)?/Kk\bsod=EI*IY`]9Y[Kn8th9W`J7e)doel7@kZdPO/u$>cYrG8Qu'%1cJYK^[VI8o\=r4ciu%,u"CRECp6\P:o]SNXo7">o.aKfkO]/^:15>&Z.-f7/_HR`B4%`#e6BH>>t(lsd!hkendstream +endobj +xref +0 13 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000525 00000 n +0000000729 00000 n +0000000933 00000 n +0000001001 00000 n +0000001281 00000 n +0000001352 00000 n +0000002574 00000 n +0000003955 00000 n +trailer +<< +/ID +[<17db83c08ab1d93c9fc574ea6beeb56b><17db83c08ab1d93c9fc574ea6beeb56b>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 8 0 R +/Root 7 0 R +/Size 13 +>> +startxref +5271 +%%EOF diff --git a/mini-crm/TP4_rempli.pdf b/mini-crm/TP4_rempli.pdf new file mode 100644 index 0000000..3fdd182 Binary files /dev/null and b/mini-crm/TP4_rempli.pdf differ diff --git a/mini-crm/TP4_walkthrough_complet.pdf b/mini-crm/TP4_walkthrough_complet.pdf new file mode 100644 index 0000000..7d25ff3 --- /dev/null +++ b/mini-crm/TP4_walkthrough_complet.pdf @@ -0,0 +1,137 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 5 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +6 0 obj +<< +/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/PageMode /UseNone /Pages 11 0 R /Type /Catalog +>> +endobj +10 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261007003136+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261007003136+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +11 0 obj +<< +/Count 4 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages +>> +endobj +12 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1436 +>> +stream +Gatm;>uU&I&:N_Clm'22'M8:QU`nMg#GqOGFJdM'?!@W0rWrLRgZe@FG9u"[@0gklSI@LKg1QJlSs9uo'L0h>:NCuXA!gUY?l\/`0UK9\?bnK]Grk17TajK/n*^o;F<0KFEUi#k5Wfs=e:hY4?XQ#XpH7I,-j7hP*s=-F3/>t-jheDTe80,O%5ZXoN7O40-7%"9#G?d1r<[[RLmb']]#AtV!8J&*gdT;0:U34]2^PZ*J^C6#3Mg\V@\A4dV3%C1ObVp(Vk?Nus'#51G8CAnM,N3s&N49)AC.'I9JcdcO($jS*MgSm$tX7++VI09PbR.\n7-M4,0%;^"M!78ZFTl^LlLR2476h%gXm0G9,kO2.ceTn_YC-t!_R=^dB"gSr@ASO'3_TfFu%lRY1F"(1NXi'FXPDb$[u?5F4.>/C>?tCCb'>""Z!:(/^J./KNY`<5cMe:dqE/^[+;&7X+5tao"6sY.$iQY=K'Q6\b%EEGh4p.;rj!nm-`W;nmrlktWt3G7+-hTHC36liM:l,O1VPh-F#u?%`Zj2bEA+W:^[=KQR"43DB!uI/"Lks(&&P(qfHO75fc/]spr`H5sO^pY/O]<$!k,-eQ'&BX8`tbmJ?i>.,a\B:&ft^t97]>[`2Omlf:a[p#-oOf&&o9I+HK?)8)ETLK6V5GT^ASNjfh&bHA&*Wr>7sBqS,])u3eW#IU&V86JA4]9]*ojATNje()2Ibi0$9sX8(1#s"q$YkcWQe0397k7rPTSX,Ui56K3l.riO!oq,V4q4*287c=VbUG2D?Y"3*aX-St=hG_^Y4eN=_XGq$kK-DXDpefBinOB47C\$<\.L,Yg>HdP)&d+=pDuEKK7,6l;?Pq.9F'\D-N>189kX@(F1I`=-Xr_4lK<[]#<@HD0:iBX'krN9"OcSj$^7P,R:^YTi135YOT^-+n1J/J-BF!^$oo?=YM'!GI/:8&+I*dPuq7A6ti]E/\Fr1#sm/d"25\29\2T@\M1kK$M@k*.:LHkj:<>n$g<=f[_d`u:*;G#ma'WpmfDM$cth5iZn$qa(=PeAYIR_N2BF7qiE8Xo!2HlnI67#u]aUsY[?`NQk]%'pATDTg`("I*RMS'*jcd]=L]\-X*Wj$$#]_+;RhRYYjDN&)55kja612aS+1cSP."l$_->^.6Uj-hK;@`n9im+6)3tieQsZj_P'M&XeZNLj(suH.EoW(?[&kl;@r]CLjh^^-XL4_n&]7`;cu_,p\>Ri;RUl*a@n4?oWuhGGd"N"Ne:'X"Ej;O%goLi6$fU]s,ecl'6RNjH?qL+@*'g=I=K&Q1c?2W&*h9c.&*C!#9;>O$lbf1h%OHR2b!o)$]u9acg#%V-`Je`8E$i_Tj%GNu.(#SIpAeh.U?=&_[hk[o0b]FDZ7ia)C*L_]1Gi(=l'B8)d9+""tiUD\,ll/bYj?d[09l&64g?Q/]3St')5X\k^=)>[[_#5'$(o'G9o`\t.d%cQR=0BIR4e]5EJbdXnendstream +endobj +14 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1239 +>> +stream +Gatm:9lJcG&A@7.b[H(6MhKKU-0J*H2bj%0m(/&.P:s!oZC.O[Kr!MthsZYi*BRQB"r\>_+d&:?oB+;m@u0=^L[V5Hpi?J&+(L*?=>US%/;(nngapaBcGl-t0?i'jI*^4r9B-F_7R(Sf/<=CSP_!!5"7[3ka`B@7AgJ]/RheFt^nQP)@\1E,:u^L2rXLQRj"#fT/M%rOn[jk\M6_"Dj;Em!Os1T/_X]P0#V@X7BGSRn.BkYOb;(4XKu`d/I0>s6Gkj(fP!WTRLHh1^\A&"@!Tll'IH:r:s*sUHge/]Bjd2t+JsA\]U+p+%OWiVLk?9BK[$57>?tW)Lc/EY$bFaeL'<,kE,)l<6#r9"D5qi#ElLIiK8m;mjK\X/L=6Y=LGBtU,G?(jUUK>2RKe!Z_Xm7fB,1(P/69Fr#:ZtZr](Uag1Z$ks6&>Yl/)E0p2`U0-DB"*^iPQCc]4ms-hJ\M"*E"RDU1I)A-FgpY6a[FP#M(k_N!%ZN.#iVB7"'=YVI8Dc'=!<:Uc,@O[]`gUEM_m-SF8hlqfAhh]UXYk%5>8)SE0E=ZI%Qjof8YdHb(Q-%iu.7]a*Y:HsVA4Z2:;L>I0XO2K):^N@^(U[Y;G5DV!YRP(cNhkn13)4>AHoY.b0j>e20J):sL4$#qLR%p\jK.h\f^YN%=A,&[k*58=7aj<'?G&otXZKuh\8%eAXP!0D/KbZPS(SR/fEl&Yendstream +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 891 +>> +stream +Gatm8_+s#E&A@ZMF"FBO->CmRgm1i579eK]$#*t@$(+HR?mdLt^LJXn!::,h79("6`h%H3mu9[?kSO)nBE6GPu#6IETEfYh_XPrr2e0B!%;IZG$^6h(H$2])@KZL/q_0ak=H,,AqkP'IZR>&pql&&N\pC`Xj)((lMTiH^bRJCl0<>sqYT0._26-TQeu1pW9rG>]riUuF8G61B(,;>^bgi8*iXc9mm,8ZA0[)uJmK[LXs/dn.^t(u9C\>"dV$d&N8+60Oi`6;;qc/@VZc_;OuHZ?F"a?Vo)ZWm;P6?jBO5gfcP;,.k&?,[eGN:DO)n2bYV"D&!R0(p#1aE-Ae*4pPL-DZB$G;jN3b1K'7f_DL@SK5tc,Q6jFEq:B67#s3\iT.tMW+-7YXM_2nTK+O#S#>A/@tM&Fj+nb]([M4)5?QLqg0pbIkqh+3mc#=)jWUB<*3R:f5&dM_(BV%&Rt>Af,^N?pGQAL0J3UX*+&Wk2#a?Vk`f7gsY`)/Mlc[,Cn@gMOV.U$Nendstream +endobj +xref +0 16 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000536 00000 n +0000000641 00000 n +0000000846 00000 n +0000001051 00000 n +0000001256 00000 n +0000001325 00000 n +0000001606 00000 n +0000001684 00000 n +0000003212 00000 n +0000004553 00000 n +0000005884 00000 n +trailer +<< +/ID +[<0b000bddee687e04fb91f32cbd1a9a63><0b000bddee687e04fb91f32cbd1a9a63>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 10 0 R +/Root 9 0 R +/Size 16 +>> +startxref +6866 +%%EOF diff --git a/mini-crm/TP5_rempli.pdf b/mini-crm/TP5_rempli.pdf new file mode 100644 index 0000000..2ce0464 Binary files /dev/null and b/mini-crm/TP5_rempli.pdf differ diff --git a/mini-crm/TP5_walkthrough_complet.pdf b/mini-crm/TP5_walkthrough_complet.pdf new file mode 100644 index 0000000..dca54e3 Binary files /dev/null and b/mini-crm/TP5_walkthrough_complet.pdf differ diff --git a/mini-crm/TP6_rempli.pdf b/mini-crm/TP6_rempli.pdf new file mode 100644 index 0000000..e6e6741 Binary files /dev/null and b/mini-crm/TP6_rempli.pdf differ diff --git a/mini-crm/TP6_walkthrough_complet.pdf b/mini-crm/TP6_walkthrough_complet.pdf new file mode 100644 index 0000000..be453f7 Binary files /dev/null and b/mini-crm/TP6_walkthrough_complet.pdf differ diff --git a/mini-crm/app/Http/Controllers/ClientController.php b/mini-crm/app/Http/Controllers/ClientController.php new file mode 100644 index 0000000..f118590 --- /dev/null +++ b/mini-crm/app/Http/Controllers/ClientController.php @@ -0,0 +1,70 @@ +query('search', ''); + $search = is_string($searchInput) ? trim($searchInput) : ''; + $query = Client::query(); + + if ($search !== '') { + $query->where(function ($query) use ($search) { + $query->where('name', 'like', '%'.$search.'%') + ->orWhere('email', 'like', '%'.$search.'%') + ->orWhere('city', 'like', '%'.$search.'%'); + }); + } + + $clients = $query->orderBy('id')->paginate(10)->withQueryString(); + + return view('clients.index', compact('clients', 'search')); + } + + // TP4 : formulaires et actions du CRUD (créer, voir, modifier, supprimer). + public function create() + { + return view('clients.create'); + } + + // TP6 : Laravel valide le Form Request avant d'appeler cette méthode. + public function store(StoreClientRequest $request) + { + Client::create($request->validated()); + + return redirect()->route('clients.index'); + } + + public function show(Client $client) + { + return view('clients.show', compact('client')); + } + + public function edit(Client $client) + { + return view('clients.edit', compact('client')); + } + + public function update(UpdateClientRequest $request, Client $client) + { + $client->update($request->validated()); + + return redirect()->route('clients.index'); + } + + public function destroy(Client $client) + { + $client->delete(); + + return redirect()->route('clients.index'); + } + +} diff --git a/mini-crm/app/Http/Controllers/ClientsController.php b/mini-crm/app/Http/Controllers/ClientsController.php index 8c1929a..c1fe0cb 100644 --- a/mini-crm/app/Http/Controllers/ClientsController.php +++ b/mini-crm/app/Http/Controllers/ClientsController.php @@ -6,40 +6,26 @@ class ClientsController extends Controller { public function index() { - { - // $clients = Client::all(); - - // return view('clients.index', [ - // 'clients' => $clients - // ]); - // } - - // $clients = Client::where('city', 'Paris')->get(); - // $clients = Client::where('name', 'like', '%Martin%')->get(); - $clients = Client::find(1); - return view('clients.index', [ - 'clients' => $clients - ]); - } - - // $clients = [ - // [ - // 'name' => 'Jean Dupont', - // 'email' => 'jean@example.com', - // 'city' => 'Paris' - // ], - // [ - // 'name' => 'Marie Martin', - // 'email' => 'marie@example.com', - // 'city' => 'Lyon' - // ], - // [ - // 'name' => 'Paul Durand', - // 'email' => 'paul@example.com', - // 'city' => 'Marseille' - // ] - // ]; - - // return view('clients', ['clients' => $clients]); + return $this->renderClients(Client::all()); } -} \ No newline at end of file + + public function paris() + { + return $this->renderClients(Client::where('city', 'Paris')->get()); + } + + public function martin() + { + return $this->renderClients(Client::where('name', 'like', '%Martin%')->get()); + } + + public function second() + { + return $this->renderClients(collect([Client::findOrFail(2)])); + } + + private function renderClients($clients) + { + return view('clients.index', compact('clients')); + } +} diff --git a/mini-crm/app/Http/Controllers/Controller.php b/mini-crm/app/Http/Controllers/Controller.php index 5467b12..a8b6f0f 100644 --- a/mini-crm/app/Http/Controllers/Controller.php +++ b/mini-crm/app/Http/Controllers/Controller.php @@ -4,11 +4,5 @@ namespace App\Http\Controllers; abstract class Controller { - public function index() - { - // ici tu mettras ton tableau $clients - - // puis tu retourneras la vue clients - } - + // TP6 : chaque contrôleur définit son propre index(), avec sa signature adaptée. } diff --git a/mini-crm/app/Http/Controllers/ProductController.php b/mini-crm/app/Http/Controllers/ProductController.php index 76ceff5..e36b992 100644 --- a/mini-crm/app/Http/Controllers/ProductController.php +++ b/mini-crm/app/Http/Controllers/ProductController.php @@ -2,48 +2,68 @@ namespace App\Http\Controllers; +use App\Models\Product; +use App\Http\Requests\StoreProductRequest; +use App\Http\Requests\UpdateProductRequest; use Illuminate\Http\Request; class ProductController extends Controller { - public function index(){ + // TP6 : recherche par nom ou prix (bonus), avec pagination de dix produits. + public function index(Request $request) + { + $searchInput = $request->query('search', ''); + $search = is_string($searchInput) ? trim($searchInput) : ''; + $query = Product::query(); - $products = Product::all(); - return view('product.index', [ - 'product' => $products - ]); + if ($search !== '') { + $query->where(function ($query) use ($search) { + $query->where('name', 'like', '%'.$search.'%') + ->orWhere('price', 'like', '%'.$search.'%'); + }); + } - // $products = [ - // [ - // 'name' => 'Ordinateur portable', - // 'price' => 899, - // 'stock' => 12 - // ], - // [ - // 'name' => 'Écran', - // 'price' => 249, - // 'stock' => 7 - // ], - // [ - // 'name' => 'Clavier', - // 'price' => 79, - // 'stock' => 25 - // ], - // [ - // 'name' => 'Souris', - // 'price' => 39, - // 'stock' => 42 - // ], - // [ - // 'name' => 'Webcam', - // 'price' => 99, - // 'stock' => 4 - // ] - // ]; + $products = $query->orderBy('id')->paginate(10)->withQueryString(); - // return view('products.index', ['products' => $products]); - - // } - + return view('products.index', compact('products', 'search')); } -} \ No newline at end of file + + // TP4 : les six autres actions complètent le CRUD des produits. + public function create() + { + return view('products.create'); + } + + // TP6 : seules les données validées par le Form Request sont enregistrées. + public function store(StoreProductRequest $request) + { + Product::create($request->validated()); + + return redirect()->route('products.index'); + } + + public function show(Product $product) + { + return view('products.show', compact('product')); + } + + public function edit(Product $product) + { + return view('products.edit', compact('product')); + } + + public function update(UpdateProductRequest $request, Product $product) + { + $product->update($request->validated()); + + return redirect()->route('products.index'); + } + + public function destroy(Product $product) + { + $product->delete(); + + return redirect()->route('products.index'); + } + +} diff --git a/mini-crm/app/Http/Requests/StoreClientRequest.php b/mini-crm/app/Http/Requests/StoreClientRequest.php new file mode 100644 index 0000000..e30993e --- /dev/null +++ b/mini-crm/app/Http/Requests/StoreClientRequest.php @@ -0,0 +1,38 @@ + ['required', 'string', 'max:100'], + 'email' => ['required', 'email', Rule::unique('clients', 'email')], + 'phone' => ['nullable', 'string', 'max:20'], + 'city' => ['nullable', 'string', 'max:100'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du client est obligatoire.', + 'name.max' => 'Le nom ne peut pas dépasser 100 caractères.', + 'email.required' => 'L’adresse e-mail est obligatoire.', + 'email.email' => 'L’adresse e-mail n’est pas valide.', + 'email.unique' => 'Cette adresse e-mail est déjà utilisée.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 100 caractères.', + ]; + } +} diff --git a/mini-crm/app/Http/Requests/StoreProductRequest.php b/mini-crm/app/Http/Requests/StoreProductRequest.php new file mode 100644 index 0000000..c3a41c9 --- /dev/null +++ b/mini-crm/app/Http/Requests/StoreProductRequest.php @@ -0,0 +1,37 @@ + ['required', 'string', 'max:150'], + 'price' => ['required', 'numeric', 'min:0'], + 'stock' => ['required', 'integer', 'min:0'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du produit est obligatoire.', + 'name.max' => 'Le nom ne peut pas dépasser 150 caractères.', + 'price.required' => 'Le prix est obligatoire.', + 'price.numeric' => 'Le prix doit être un nombre.', + 'price.min' => 'Le prix doit être supérieur ou égal à zéro.', + 'stock.required' => 'Le stock est obligatoire.', + 'stock.integer' => 'Le stock doit être un entier.', + 'stock.min' => 'Le stock doit être supérieur ou égal à zéro.', + ]; + } +} diff --git a/mini-crm/app/Http/Requests/UpdateClientRequest.php b/mini-crm/app/Http/Requests/UpdateClientRequest.php new file mode 100644 index 0000000..f9d15ca --- /dev/null +++ b/mini-crm/app/Http/Requests/UpdateClientRequest.php @@ -0,0 +1,41 @@ +route('client'); + + return [ + 'name' => ['required', 'string', 'max:100'], + 'email' => ['required', 'email', Rule::unique('clients', 'email')->ignore($client instanceof Client ? $client : null)], + 'phone' => ['nullable', 'string', 'max:20'], + 'city' => ['nullable', 'string', 'max:100'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du client est obligatoire.', + 'name.max' => 'Le nom ne peut pas dépasser 100 caractères.', + 'email.required' => 'L’adresse e-mail est obligatoire.', + 'email.email' => 'L’adresse e-mail n’est pas valide.', + 'email.unique' => 'Cette adresse e-mail est déjà utilisée.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 100 caractères.', + ]; + } +} diff --git a/mini-crm/app/Http/Requests/UpdateProductRequest.php b/mini-crm/app/Http/Requests/UpdateProductRequest.php new file mode 100644 index 0000000..a9a9055 --- /dev/null +++ b/mini-crm/app/Http/Requests/UpdateProductRequest.php @@ -0,0 +1,37 @@ + ['required', 'string', 'max:150'], + 'price' => ['required', 'numeric', 'min:0'], + 'stock' => ['required', 'integer', 'min:0'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du produit est obligatoire.', + 'name.max' => 'Le nom ne peut pas dépasser 150 caractères.', + 'price.required' => 'Le prix est obligatoire.', + 'price.numeric' => 'Le prix doit être un nombre.', + 'price.min' => 'Le prix doit être supérieur ou égal à zéro.', + 'stock.required' => 'Le stock est obligatoire.', + 'stock.integer' => 'Le stock doit être un entier.', + 'stock.min' => 'Le stock doit être supérieur ou égal à zéro.', + ]; + } +} diff --git a/mini-crm/app/Models/Client.php b/mini-crm/app/Models/Client.php index 6158241..8ff9849 100644 --- a/mini-crm/app/Models/Client.php +++ b/mini-crm/app/Models/Client.php @@ -6,9 +6,11 @@ use Illuminate\Database\Eloquent\Model; class Client extends Model { + // TP3 : champs autorisés pour créer un client ; TP5 : ajout du téléphone. protected $fillable = [ 'name', 'email', + 'phone', 'city', ]; } diff --git a/mini-crm/app/Models/Product.php b/mini-crm/app/Models/Product.php index 84723db..29e3d43 100644 --- a/mini-crm/app/Models/Product.php +++ b/mini-crm/app/Models/Product.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; class Product extends Model { + // TP3 : champs autorisés pour créer les produits du seeder. protected $fillable = [ 'name', 'price', diff --git a/mini-crm/composer.json b/mini-crm/composer.json index 9b8fbd5..12cf7f3 100644 --- a/mini-crm/composer.json +++ b/mini-crm/composer.json @@ -12,6 +12,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", + "laravel/boost": "^2.10", "laravel/pail": "^1.2.5", "laravel/pao": "^1.0.6", "laravel/pint": "^1.27", diff --git a/mini-crm/composer.lock b/mini-crm/composer.lock index 9d5bb07..193c9cf 100644 --- a/mini-crm/composer.lock +++ b/mini-crm/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cda2add6397422fb1a2eb5b9207f1575", + "content-hash": "b95aef2242bb3dce2389c238b7ec69bf", "packages": [ { "name": "brick/math", @@ -6029,6 +6029,83 @@ } ], "packages-dev": [ + { + "name": "composer/semver", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "528948210cc46f967a4b98e83b19e76f2b504993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/528948210cc46f967a4b98e83b19e76f2b504993", + "reference": "528948210cc46f967a4b98e83b19e76f2b504993", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.5.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-10-05T13:12:12+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", @@ -6280,6 +6357,147 @@ }, "time": "2026-04-29T18:32:34+00:00" }, + { + "name": "laravel/boost", + "version": "v2.10.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "2379e52b09bbdd2e72a5bf392891449be2f9f906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/2379e52b09bbdd2e72a5bf392891449be2f9f906", + "reference": "2379e52b09bbdd2e72a5bf392891449be2f9f906", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9|^8.0", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.7.1|^0.8.0|^0.9.0|^1.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^1.0.0", + "php": "^8.2" + }, + "require-dev": { + "jasonmccreary/double": "^1.0", + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-10-05T13:34:47+00:00" + }, + { + "name": "laravel/mcp", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "92987a9d03847801299ff4abe909ba7686147dde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/92987a9d03847801299ff4abe909ba7686147dde", + "reference": "92987a9d03847801299ff4abe909ba7686147dde", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2", + "symfony/process": "^7.4.5|^8.0.5" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-09-24T10:45:09+00:00" + }, { "name": "laravel/pail", "version": "v1.2.7", @@ -6515,6 +6733,68 @@ }, "time": "2026-08-10T15:35:50+00:00" }, + { + "name": "laravel/roster", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "89e518bd88ae98ff50f6082f6b517c8d8e8245fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/89e518bd88ae98ff50f6082f6b517c8d8e8245fa", + "reference": "89e518bd88ae98ff50f6082f6b517c8d8e8245fa", + "shasum": "" + }, + "require": { + "composer/semver": "^3.0", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.29", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-07-18T17:53:15+00:00" + }, { "name": "mockery/mockery", "version": "1.6.15", @@ -8279,6 +8559,82 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/yaml", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "030e96eed0303223bfca3af9109e9e64830b6ec3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/030e96eed0303223bfca3af9109e9e64830b6ec3", + "reference": "030e96eed0303223bfca3af9109e9e64830b6ec3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:33:49+00:00" + }, { "name": "theseer/tokenizer", "version": "2.0.1", diff --git a/mini-crm/database/migrations/2026_09_16_075551_create_clients_table.php b/mini-crm/database/migrations/2026_09_16_075551_create_clients_table.php index c95dd6b..1833f3e 100644 --- a/mini-crm/database/migrations/2026_09_16_075551_create_clients_table.php +++ b/mini-crm/database/migrations/2026_09_16_075551_create_clients_table.php @@ -11,6 +11,7 @@ return new class extends Migration */ public function up(): void { + // TP3 : structure initiale de la table clients ; le TP5 l'adapte ensuite. Schema::create('clients', function (Blueprint $table) { $table->id(); $table->string('name'); diff --git a/mini-crm/database/migrations/2026_09_16_085026_create_product_table.php b/mini-crm/database/migrations/2026_09_16_085026_create_product_table.php index 36cd0a4..b46b2eb 100644 --- a/mini-crm/database/migrations/2026_09_16_085026_create_product_table.php +++ b/mini-crm/database/migrations/2026_09_16_085026_create_product_table.php @@ -11,7 +11,8 @@ return new class extends Migration */ public function up(): void { - Schema::create('product', function (Blueprint $table) { + // TP3 : table products au pluriel, nom attendu par le Model Product. + Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->unsignedInteger('price'); @@ -35,6 +36,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('product'); + Schema::dropIfExists('products'); } }; diff --git a/mini-crm/database/migrations/2026_10_07_000000_adapt_clients_and_products_for_tp5.php b/mini-crm/database/migrations/2026_10_07_000000_adapt_clients_and_products_for_tp5.php new file mode 100644 index 0000000..1b70dd3 --- /dev/null +++ b/mini-crm/database/migrations/2026_10_07_000000_adapt_clients_and_products_for_tp5.php @@ -0,0 +1,33 @@ +string('phone', 20)->nullable(); + $table->string('city', 100)->nullable()->change(); + }); + + Schema::table('products', function (Blueprint $table) { + $table->decimal('price', 10, 2)->change(); + }); + } + + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropColumn('phone'); + $table->string('city')->nullable(false)->change(); + }); + + Schema::table('products', function (Blueprint $table) { + $table->unsignedInteger('price')->change(); + }); + } +}; diff --git a/mini-crm/database/seeders/ClientSeeder.php b/mini-crm/database/seeders/ClientSeeder.php index 3dc3b10..e3132a6 100644 --- a/mini-crm/database/seeders/ClientSeeder.php +++ b/mini-crm/database/seeders/ClientSeeder.php @@ -12,6 +12,7 @@ class ClientSeeder extends Seeder */ public function run(): void { + // TP3 : trois clients de démonstration pour tester Eloquent et la vue. Client::create([ 'name' => 'Jean Dupont', 'email' => 'jean@example.com', diff --git a/mini-crm/database/seeders/DatabaseSeeder.php b/mini-crm/database/seeders/DatabaseSeeder.php index 09f3fbc..700c99f 100644 --- a/mini-crm/database/seeders/DatabaseSeeder.php +++ b/mini-crm/database/seeders/DatabaseSeeder.php @@ -15,15 +15,12 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // // User::factory(10)->create(); - // $this->call([ - // ClientSeeder::class, - // ]); + // TP3 : remplit les tables clients et products avec les exemples du sujet. + $this->call([ + ClientSeeder::class, + ProductSeeder::class, + ]); - $this->call([ - ProductSeeder::class, - ]); - User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', diff --git a/mini-crm/database/seeders/ProductSeeder.php b/mini-crm/database/seeders/ProductSeeder.php index 5507ed9..0cc1a94 100644 --- a/mini-crm/database/seeders/ProductSeeder.php +++ b/mini-crm/database/seeders/ProductSeeder.php @@ -12,6 +12,7 @@ class ProductSeeder extends Seeder */ public function run(): void { + // TP3 : cinq produits demandés dans l'exercice final. Product::create([ 'name' => 'Ordinateur', 'price' => 899, @@ -42,4 +43,4 @@ class ProductSeeder extends Seeder 'stock' => 4 ]); } -} \ No newline at end of file +} diff --git a/mini-crm/resources/views/clients/create.blade.php b/mini-crm/resources/views/clients/create.blade.php new file mode 100644 index 0000000..380fc84 --- /dev/null +++ b/mini-crm/resources/views/clients/create.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.app') + +@section('content') +

Ajouter un client

+ +{{-- TP5 : résumé des erreurs de validation renvoyées par Laravel. --}} +@if ($errors->any()) +
+ Des erreurs sont présentes : +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+@endif + +
+ {{-- TP4 : protection CSRF du formulaire de création. --}} + @csrf + + {{-- TP5 : old() conserve la saisie ; @error affiche le problème sous chaque champ. --}} + + + @error('name')
{{ $message }}
@enderror + + + + @error('email')
{{ $message }}
@enderror + + + + @error('phone')
{{ $message }}
@enderror + + + + @error('city')
{{ $message }}
@enderror + + +
+@endsection diff --git a/mini-crm/resources/views/clients/edit.blade.php b/mini-crm/resources/views/clients/edit.blade.php new file mode 100644 index 0000000..7f3200e --- /dev/null +++ b/mini-crm/resources/views/clients/edit.blade.php @@ -0,0 +1,42 @@ +@extends('layouts.app') + +@section('content') +

Modifier {{ $client->name }}

+ +{{-- TP5 : messages de validation visibles aussi lors d'une modification. --}} +@if ($errors->any()) +
+ Des erreurs sont présentes : +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+@endif + +
+ {{-- TP4 : un formulaire HTML POST représente ici une mise à jour PUT. --}} + @csrf + @method('PUT') + + {{-- TP5 : old() reprend la saisie refusée ou, sinon, la valeur du client. --}} + + + @error('name')
{{ $message }}
@enderror + + + + @error('email')
{{ $message }}
@enderror + + + + @error('phone')
{{ $message }}
@enderror + + + + @error('city')
{{ $message }}
@enderror + + +
+@endsection diff --git a/mini-crm/resources/views/clients/index.blade.php b/mini-crm/resources/views/clients/index.blade.php index 9ef3890..0e968c4 100644 --- a/mini-crm/resources/views/clients/index.blade.php +++ b/mini-crm/resources/views/clients/index.blade.php @@ -1,19 +1,45 @@ @extends('layouts.app') @section('content') -

Liste des clients :

+

Liste des clients

+{{-- TP4 : total, date et actions ; TP6 : total de tous les résultats, même sur plusieurs pages. --}} +

Nombre total de clients : {{ $clients->total() }}

+

Ajouter un client

+{{-- TP6 : GET rend la recherche visible dans l'URL sans modifier la base. --}} +
+ + + + Effacer +
- + + + + + - - + @foreach($clients as $client) - - + + + + + + - + @endforeach
NomEmailTéléphoneVilleClient créé leActions
{{ $clients->name }}{{ $client->name }}{{ $client->email }}{{ $client->phone ?? '—' }}{{ $client->city ?? '—' }}{{ $client->created_at->format('d/m/Y') }} + Voir + Modifier +
+ @csrf + @method('DELETE') + +
+
+{{-- TP6 : liens générés pour les dix résultats de chaque page. --}} +{{ $clients->links() }} @endsection diff --git a/mini-crm/resources/views/clients/show.blade.php b/mini-crm/resources/views/clients/show.blade.php new file mode 100644 index 0000000..7c997e6 --- /dev/null +++ b/mini-crm/resources/views/clients/show.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') +

{{ $client->name }}

+

Email : {{ $client->email }}

+

Téléphone : {{ $client->phone ?? '—' }}

+

Ville : {{ $client->city ?? '—' }}

+

Client créé le : {{ $client->created_at->format('d/m/Y') }}

+Retour +@endsection diff --git a/mini-crm/resources/views/layouts/app.blade.php b/mini-crm/resources/views/layouts/app.blade.php index 244a570..e65683c 100644 --- a/mini-crm/resources/views/layouts/app.blade.php +++ b/mini-crm/resources/views/layouts/app.blade.php @@ -3,14 +3,20 @@ Mini-CRM +{{-- TP6 : taille lisible des flèches SVG utilisées par la pagination Laravel. --}} +

Mini-CRM