From 02c29db6964fc13646cac0b9886be29517e1af26 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Thu, 3 Sep 2026 06:04:20 +0330 Subject: [PATCH] feat(core): implement order approval flow, credit system, and import invoicing Introduce a comprehensive set of commercial features including a multi-step order approval workflow, customer credit management, and specialized import service invoicing. Key changes: - Implement `pending_approval` and `approved` shipment statuses to allow staff verification before customer payment. - Add a credit system to `User` model with `credit_limit` and `credit_used` to manage customer balances and debts. - Develop a new `importInvoice` PDF generation service following the "Sheet ENG Invoice" specification for import services. - Add Filament resources for managing Audit Logs, Commitment Forms, Customer Credits, and Shipment Checklists. - Implement staff-specific APIs for order approval/rejection and customer financial status monitoring. - Integrate Kavenegar SMS service for mobile verification and notifications. - Add bulk tracking import functionality via CSV/Excel. - Update WordPress bridge assets (CSS/JS) to support the new multi-step order form UI and updated redirection logic. - Update deployment configurations and documentation to reflect new production domains and feature sets. --- 01_Documents/Sheet ENG invoice.pdf | Bin 0 -> 55251 bytes 01_Documents/Sheet ENG invoice.xlsx | Bin 0 -> 47644 bytes .../ifnex-bridge/assets/css/ifnex-bridge.css | 161 ++++++++ .../assets/js/ifnex-order-form.js | 4 +- .../ifnex-bridge/includes/shortcodes.php | 70 +++- 04_Laravel/app/Enums/ShipmentStatus.php | 52 ++- .../app/Filament/Pages/BulkTrackingImport.php | 198 ++++++++++ .../Filament/Resources/AuditLogResource.php | 174 +++++++++ .../AuditLogResource/Pages/EditAuditLog.php | 11 + .../AuditLogResource/Pages/ListAuditLogs.php | 17 + .../Resources/CommitmentFormResource.php | 157 ++++++++ .../Pages/CreateCommitmentForm.php | 17 + .../Pages/EditCommitmentForm.php | 19 + .../Pages/ListCommitmentForms.php | 19 + .../Resources/CustomerCreditResource.php | 242 ++++++++++++ .../Pages/EditCustomerCredit.php | 19 + .../Pages/ListCustomerCredits.php | 19 + .../Resources/ShipmentChecklistResource.php | 196 ++++++++++ .../Pages/CreateShipmentChecklist.php | 11 + .../Pages/EditShipmentChecklist.php | 19 + .../Pages/ListShipmentChecklists.php | 19 + .../Filament/Resources/ShipmentResource.php | 71 ++++ .../Api/CommitmentFormController.php | 66 ++++ .../Api/Customer/CustomerOrderController.php | 40 +- .../Api/CustomerFinancialController.php | 135 +++++++ .../Api/MobileVerificationController.php | 104 ++++++ .../Controllers/Api/StaffOrderController.php | 201 ++++++++++ .../Controllers/ShipmentPdfController.php | 52 +++ 04_Laravel/app/Models/AuditLog.php | 65 ++++ 04_Laravel/app/Models/CommitmentForm.php | 61 +++ 04_Laravel/app/Models/Shipment.php | 12 + 04_Laravel/app/Models/ShipmentChecklist.php | 48 +++ 04_Laravel/app/Models/User.php | 16 + .../app/Services/KavenegarSmsService.php | 169 +++++++++ 04_Laravel/app/Services/PdfService.php | 188 ++++++++++ 04_Laravel/app/Traits/Auditable.php | 37 ++ 04_Laravel/config/services.php | 5 + ...d_approval_statuses_to_shipments_table.php | 60 +++ ...3_000002_create_commitment_forms_table.php | 33 ++ ...00003_add_credit_fields_to_users_table.php | 23 ++ ...00004_create_shipment_checklists_table.php | 27 ++ ...6_09_03_000005_create_audit_logs_table.php | 34 ++ ...port_invoice_fields_to_shipments_table.php | 38 ++ 04_Laravel/read_excel.php | 26 ++ .../pages/bulk-tracking-import.blade.php | 44 +++ .../views/pdfs/import_invoice.blade.php | 353 ++++++++++++++++++ 04_Laravel/routes/api.php | 31 +- 04_Laravel/routes/web.php | 1 + AGENT.md | 259 +++++++++++++ DEPLOYMENT.md | 20 +- README.md | 10 + 51 files changed, 3604 insertions(+), 49 deletions(-) create mode 100644 01_Documents/Sheet ENG invoice.pdf create mode 100644 01_Documents/Sheet ENG invoice.xlsx create mode 100644 04_Laravel/app/Filament/Pages/BulkTrackingImport.php create mode 100644 04_Laravel/app/Filament/Resources/AuditLogResource.php create mode 100644 04_Laravel/app/Filament/Resources/AuditLogResource/Pages/EditAuditLog.php create mode 100644 04_Laravel/app/Filament/Resources/AuditLogResource/Pages/ListAuditLogs.php create mode 100644 04_Laravel/app/Filament/Resources/CommitmentFormResource.php create mode 100644 04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/CreateCommitmentForm.php create mode 100644 04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/EditCommitmentForm.php create mode 100644 04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/ListCommitmentForms.php create mode 100644 04_Laravel/app/Filament/Resources/CustomerCreditResource.php create mode 100644 04_Laravel/app/Filament/Resources/CustomerCreditResource/Pages/EditCustomerCredit.php create mode 100644 04_Laravel/app/Filament/Resources/CustomerCreditResource/Pages/ListCustomerCredits.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/CreateShipmentChecklist.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/EditShipmentChecklist.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/ListShipmentChecklists.php create mode 100644 04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php create mode 100644 04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php create mode 100644 04_Laravel/app/Http/Controllers/Api/MobileVerificationController.php create mode 100644 04_Laravel/app/Http/Controllers/Api/StaffOrderController.php create mode 100644 04_Laravel/app/Models/AuditLog.php create mode 100644 04_Laravel/app/Models/CommitmentForm.php create mode 100644 04_Laravel/app/Models/ShipmentChecklist.php create mode 100644 04_Laravel/app/Services/KavenegarSmsService.php create mode 100644 04_Laravel/app/Traits/Auditable.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000001_add_approval_statuses_to_shipments_table.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000002_create_commitment_forms_table.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000003_add_credit_fields_to_users_table.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000004_create_shipment_checklists_table.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000005_create_audit_logs_table.php create mode 100644 04_Laravel/database/migrations/2026_09_03_000006_add_import_invoice_fields_to_shipments_table.php create mode 100644 04_Laravel/read_excel.php create mode 100644 04_Laravel/resources/views/filament/pages/bulk-tracking-import.blade.php create mode 100644 04_Laravel/resources/views/pdfs/import_invoice.blade.php create mode 100644 AGENT.md diff --git a/01_Documents/Sheet ENG invoice.pdf b/01_Documents/Sheet ENG invoice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..283ed68304b46166035284b9158fa80ec6c16f7e GIT binary patch literal 55251 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

R!#&D8*UY=a`atYVy4EXoM{s0aT7L0 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 O4q8G2BL)9H=>G!VuWE<@ literal 0 HcmV?d00001 diff --git a/01_Documents/Sheet ENG invoice.xlsx b/01_Documents/Sheet ENG invoice.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..99b3979ba3e7b1db4d0c398082a55fa08493fda9 GIT binary patch literal 47644 zcmeFY|@oZoHl%E@>qME&1j3 z#@LBA;+Ftc6?xS!u*A0X9v@csRS+4`eVSsa#mT+GB*~={f@D4d*6C?)r@rzBdqUtc zQW|;Pm=QHacLNeU`%1!+#2*}?$%nrk2PL!;(g_tdZC099ll>5?6#9k}wG+Q4j1G<+ z3*|kV9VE0%5D=Xz5ZBh2M1-Sm@zP;w6DSS@7sA*c9lfC{qs*8!Dr6;Wu}t+EMwRG? zn>StzqZzTHhj2x{;NYkUv8jDu+p^qyX<{oMrsSp-Py1$>UlIMfxW;(2VPxjP(t_Uu zv~!?GHyi=oUFis}XFw0mv<3J+=nRn@}r3O!~3gAy4Xm}!9JTTUFb4yO-E%Up_ zrxjnrJ{RiF2iCU=XBUu!I5=-FuVYxwwvRSMvM+j*IS=R@bNN1D0RXEkX*8$fEN83{^5n(JF z(uf|&8u$ccjh*_5G7=>n-kx)T9rBj&-QOSgpZJa2GX0V4lS2y1byXG!7_b`Xo}l!X z`@iu-^l_`7P!#$Bp{+8`D<7FXreX{*wW$oC_|QOs9X;Y2(f|YLP)VbdZg3Ct+Le<$ z)0;4zV;xjhT$#SF{7jZ9T~r0P*RT;T3HLa+7M-VeQzrR-9uSk0$VJT+=oHC7GIP@q zXl8pFfu>9&@^3{)HNz5YbZ#-B8c1poo)W<~$6pmDWz zur#o>wfrZu{;vQ6{F7e)*#6JHx>9tc2N_U8u0g+rN4sU8d5yQ|Q1tWIx%xNaa+>vD zDaO)WzIADAE(IENg0@laO0xKP$ayafR4he%`rOTB#lR({ATjhE%_E|Gta7f8lH3l+ zj>oY^PXKvzXl-rLZ1XszEWIeHe^^vU&6tI&^D-QXLT0@GQvGRB-Lh;R!&hJP6BV6X z7XZ8DG^yPNwu7fqa@k#Miz`>Dfm7K|*1B^`3h;;O1uMqKF(Qlwh-sTgyovZ&NGA|O z;nrgA7PEv!HHH3nA*>kVnir&Btsl?vs20a(Du`r3fR|RQ=rmh?Xi-9 zq-JK(EYi!`^iJdz$o!o?2l!HYx8|Nh1$SI;cQZ6E*a5_nHsDRJs zfIC?YdQICFpSVfFnRLwQJ^4X+S(O0s0Z`*+__=yOz;fo4g}3d{l~eU|VFHpGN9~4; zmz#8m7s8j<(mmvTc;;a|0j1>T8-2pVfd%Jy)S`jkX|xhs%7llUpnw1!JS z(p-h4$uqQ+ASy?)SC+%KRU~{?allw`SXTid#K0_@n{F0yjmH%Qn5F?!hxWr%-lz|{ zU!${#9?hUrUwAmfM(0^PiYkNF%A4MX%2z29Tk(YZp=)vVh0HsnM^=W+`V3hM30~Tg z=(|I|anvdlNkK@_n8wOU+TlGBk}kr#m~IXYnu?6CgdP1R@jyK4mhMTxF(<|%XlTpY zG@x+_VWt0uwOQ* zvK@p9Is0BY1s@FY#5UKJg5z+{`cWhM|;J99_!tp9ywoJ6o7Ml2mU@N1rC zMXrgVWkl0R!|;G>RBp2$y$r4(m9Z;nNE0ApA=Kp>TgkL-ZIWP6Yu@Yldr1A8SeBilsR+m*??w&8=TiF{8Wp*t zW}>z;86pgKp|k&WMng@^^|10|9)>i}U_jYb>fW8yR!7Ay9SG=@V*#BfMwedc-Pe|a zTToXae@s>Ww!Tjz_I`6__7hu<*k1g7ZcRaPTon-xSnlkxYk5b+Txs;}z1oeTCVYLt^@Vb9;cS-L0$k3|czsxzDZIjZ zlS2ReXtA1V&f8T74~iHh)2>KW>%NcZ6>B})Igklba-l8O%T=YB+w~6CX|<4M481MV zZ5A8j?vU?|aca$-)Pvcj{n*f&sOg#cMq%_^veT`pm))hkH43<1UWGwgPd?>~U_pCJo|K=AeFAGi`@006-F{~^oK%-Gn;k^XPT@=wOiO4PC3 z_$SGLzrzbX*VFoPuTpBv&&KFP%~`KRc;Q4!sFG1ePptale}y5N5f-}&E#G$?ON_kg zP=k7aS+`k0$>6{fo25~?H&WlslupqMjjFFFDVKpkeE@;&(DM0?|B$+th5e~OEoum* zt>u~4S)O~?h`Alf@)f)dO?w2 zA1D%-mr^;r`0;a^ZEU+RQTaMH6jSL=fOf~B|5BQMNTT6&zeXX$1{9@@l+d_rscebQ z!r3n7^jVKoa=MABq5)269@oNW%jd|1&N%VZ`TKf4Zg%NrvJ?1Z=q+sk&&!`{V z_?G0nMtUZ;i{@sB1oA4ipDVriS#PYhBJ+dn|PY*}#$Re6$XX#1OJ8&E=?dKxwiE6pG>rFjPT_}1xAYc_Xs!00b zOdSv*)Q12AlkZ!S%_*IOYebuF?mpY6jPFWWPy}A^XTBW#Y%zLT5k`Gsy_6g`#v?Nm zz25Rq43?I@oc(7;8OX-bbR>zptLs6Dc^8VD)D}7hLOGO?eDwFZwRg`C2Q-*6{sKO+ z(mlUi4uAAvbB90}F{ZT9UxYpxThlP&Oj#1YInB|;0NYh;SPJGqqqA5DbjM$5Aa`Xh@7m$K4JW8{2A zi*5M`d;$OCf`9$6Npr(ylMUfBxmq`&&7W4$wO>;}&4L{rTvoB^NAMn(>?{l1MRlT& z!1f)kH+4h-|2_e~k~~gL$bngRr`_jK;~*g?GtM;T*~{7E>uUw4PWE&xJw4wf9dxXF zW94({bq!SP;pDSPty=KNZ(%f(mzm7$PoL(Fmui0B`w4=*Db?DYtk1XKzd!Q~UP?RS z47=CF(Q9~^s=*iwNP_>M~Q8HyvUBJ4}*e76m4BVPc&SMK)lnm@Xd+}O1RVf{Yc-%z`T^L6ubxH~y)eCPFav%P%~ec!60)JuBq zL2PI6r>e!n!S7C}e>q&3($nMH`rz$+JwBSL&9(jdd|P{vt2T++pHqevIPq!%FkYQW zM?I1vyA_um2=WHG7QqUIWb^bw(8ADX%l0O`*iGHRn(IyXHIUaY2f+9jO z3_wJKzVmsTN(D_n6wY0bT$~_zLujb+Gh2-`DIdX1fc*f*89dr`!0xzNDx~<753;!G zzG0a%_%-wiRGfyOu|S}&qaPq{wI^4`{4f^&b+E?kqig9tr^}t&)5qs+xk(&eOn9zY z9zh)3w48t{mt^D3!_XyF6N-L_-OC~um=qaYIsnQ9}Zrg$*r(6;TSJBdmo^^6N-*qJ-ib?Lc$+v*PDl9?(D zw7N65S{bFo!wxsd4~{$9%m*pYfQZE40FLQPsDf!vv8{+b+>~DqK<83X)%%*1nEO&r z|9&Y4)C#DZl@W~DBqwoFh6iaY!lT_C<+a@E6+AQmRm=&!`iJt9Ihzrt6A{^iu@px; z^tBX6GinNhklv?(3DNfUGt@WEo2d%f>d=~JD`t6HL86?{B`YYw8rOIu?8Fkd^=5E; z%k-kLY3mhTrjq1a)R?F;yR1r}W5JRfv&^jxwlb(!wH-uErVzD0s+gUZqHvp{09PU| zHDyl@%L^(EF9eDRCNbeZXJu^}L=v2)QqcVvoknN@iM@XyeQ$Z!p4gMzzdUYoNumPZ zMOCJ+cFM}J>v`8zuiAu!YkO2){PYNLqt>%d`WtJsD>fhxtAv*4 zsF6|A0#>Z8yER3GHwaB&4F?N*O?CeIL4xeuU+w;wK6mpPV}cv)6i0f~e-=l+TnP>_ z>d*j($sPGj=QmiIu75mY$yoOT=3`47)(_}4ogP|>k})ssyES7G)~d=Ip{C}HJ}e+h z$SPt6!JoW8C)vlLcPGTmIc3v~RqfIR2=LWy+zZySoKcOT?O72xQOR$(Xp+ z>qMTi5#LN0qk;Ad9zE}2VV4)n9kD|Qo(aPQaI@mQT&Br)mce%?_*`De4Uw24)C(&^ zz$npARtQ;nsF$TMpda8}EeH7_6^R&De(D}*4j2!j_?^iEoTXAm-oD)31b+^*dHGeP=nrmZam z^Vd4}X144p@?ACNv&$ni2-$SFRZ6-5eGoQN@t*;G9(^(kh%~DI7~9aFY}`V=77)XZ zN%8^qhH!Y-_0<>6`;Q3%6Hm`acXpuBzt#E!>6^?~rpFZzw8l#*4${sb%_tH!fLfTON;@=y)5GjbmZ z)S~0f7Y|8uHEM5jHtOA`^id>osawQ2ginyOy}z6qCXz{64Me^vl?E6eclhW5OJnyD zCppjrI$usCu!V&EtyQWg{aXzbmdFYcW2q|1mf-4Kz=JffMz=79saq$hetVv^IGaWJ8qdZ7Ul99N70MI+DnHMOSHu}&L5{RUw zqPA-XZAq`_CQfNY<(_~Q&|Ojt#aCn#Z@Ns&SZD#S$Y4Clu*9+r;3nt3*p zGaMrnVO%pqaRoc)$CovyB%xhT*+cfa*9*h@3n4eg`|lP-kJ*z77tT$O-b^rm zWUX@O&`G)F=$hr>vjJ|S3o<)y>d1zTqDsFYeVh}ar5h5NOWF{P+T=KB;wR1JuOM{| zRNeG->!s;zR$7}qCw;xA&Ts|~HMeHPabm4BRXWpA9~EH~BC@kBx6xm=gcw+KNwqV$myV$i{vmVzjOBA>ieh z3AyKr$-Qe1kE~FX8FKx!1W4JOlc2;`gS%hHB+h05eI6wHs zF#>+3I77bWAN1>V1+v9a8|tnHA2Wp#W*N(=DmKjl1mB#g@QqiD?_~+>`eZB5hxZFi zH{5#?;OPu%TPu#=-+g`EaC7;Hm|+m7=Q_;#+tipOOP8~ zI${(o#1AOjq(CYGR3_P3UJV-dK84eKP4O>$U_wTHmtOuS#3_~nfUUg@<{kV}1P#D;+oa+tU zy}=o?Ga^c268nzCOo$c-BhHCS+u|hjn&yltbedc~G{zahn{3GIwDkR}eEs#>q7((k z3;~3ZUa0NJDHg;l07S5~Js>KJCc_)fef4a)hLxRjcPvKMYAjP;@UwGM-l^tQdouq;`6a@+cI5bj4BVah@IwLpg{NzG0RwN69$rn|_r+Sj= z_DKL~vR0%@huNkhrVF9D`__HZV;Rz8A?+#7;^g-s#X}_Pd!-YEHL%6@r8KwAFu=u3Tpg2z?_9d$~#Wotj z)SrT~ZgE{oF;p14E3*8p__iaS&DvDguS=3W*nVtRYr|DXyu<8>(k+_M8mI4CKQJ0* z`1}OMJ82a(7YH_c2J4)~y4h6#P^fCz29Qwd6@U$WA1y}A^r4-ylpr=Y9-}HlcTT^gfgR7$Ej(9oKjZkDmatFts4O4Bh8NyoSPJ(lk z+A5ooUl(Bu9@%J8mnL%Aw5%5?(o;(lrPe<>KJ0zlw$xltOk8~ha?`PT@&<8fdoO!@ zyT6=76R9&2G%^xa7`@9{dtq!H&t@W+IV$BZ!8r#2;nq=h$0|NMsXa0`h;#ErILYvJuwGG;Y~YB7y*JkS8vQ zngg2oJ}N&mgjw6XCADl5laZoqUvn+R^{GPg35L9U?UFmzd`)45UE9tTOMxI^O1D)C zy8}^O2){TfemidaR}uTzW7_TKpkxAbVYN-xOcTsBD}M_=Yzd$-`)U1mX&MDA4BpEP z;I;BcPeQ?k$&f3JJ9)}Z-CoC})yz4P&rIpPT|9lNE)p%7ZibwpE#qsFd!*H;d%TG~ z#_%hCSwx1kvas5(pGpHf8P)R$67s0~AfLN#P2W{x3~|`iu7kstIzkXC`U(WtVuA8U zFMz2OB9QkWVV};bi3%__Mk<>tYCgQvH)_HbRwue`?A{*%UK<@q1u&x=K#gxrLKE`s zNJ9L0DkZ_yP$i)8UN7x_66>)u#I-tFLM*q1JW&li(|&c`&YE9c7ux=gEZrhX1gU07 zZyE_Y1=vkCQ=txD6Cj3!|1NDLscG2+G9Yw8-2p;bCW0dpsEmsuh@Pn_f+jV!)-Frw zkMy^_M~X9dK22h+>>o489NV0&zqxoDwGPLP4cA^Vv4zMLkM)#aye*8(xMRHUF|n4o zHI`I+hV(GEFdk4P%0^?#YVBxM+aa3DFxZgE3#U6R79J@ZC9jYX$~TEer5P zVI?o>#KP>i90K60?NqVaGsY-Tt_5)na3n0oD;cCAPK0?(f_Q{k%ay*05gIr{&W`eeohp#~Tbg7+$6903dw5lP0j zgLhF&Ofo<$OJ&5me9GmWpL=qTO~<`Rr<^Sq?tTJ3cOnNYYqwZ4`JN%>qcgkPP|1Wl zF9GT!)YEG}h<%Q-D_Yv6t{4Gwe^lljX8@g%Iz!NTJim)yZx_U>rqR;PHC|>B9T5>x5S|a; z?C{^7ia(9`7|p4{Mg}r=xn+WY;bN4ORie4;)W}=@kK7$M|LkwhTB<^y z;4Cn}084@jivuCe8=4OX-v6RnpLh_BOOUa$2(^?}Xck>6$p%hd?!(axU1B6%y&9>x z>Ife!&~VuKE@s|Q>ki3{qXfnU%OxvNN}OBr{i#7lnWid+OJg7fCOkE>K_5VrXGO$& z?TZFB;(ru7KzkEF)T@RhM~2qkSFjUQj64}FP5+~SYR{K_FGNKD&IQqyZH#DJ^p$95 z&*^qWR@+Dt(RSy`<}37E|UenufW%mR-ixw=eDM13_1Bg+E&2vcUp$io-bb>(k7}5!&#m&9qODKj3@ub+FE_C5~DR?jm z$3T$^gNzh#+UJi2=MfWh2|$Tzg^Y-aI%@(+G-9@}j*{bJqdq(!?X?j;CRkAlo7!@W zF=9@SI{-qrW%hoBGmJNOL3na^8xitwBK|N)C_GR(|Fs=PcOwU*kVZa4{S@9!t;3+G zn1twydcW-7EvU0=*$+{A%Mg{0axL$=n4_3Cc0?buh>fhr$t>8GW(avfdkU1xA3P zNj-@3`@7GbqxmfQTx+^6{2q#i#-tZYRtH4r(kW5mh>J?U*@?|sBFC8s4UIHEB1`Ds zq)BTJG6DRDLrH7vX`sd~Xk-PW+W|p@#!^~GD4eTErnwa~FaS%OngfulS6NmvQqDP6 z=JmF`nKI*X=#em$gO#BAfV~{~bEj9j70?eCs14qS@D}0pXE{iy zrw*FpwnN$R616Oy>ob;TjIgjpoIxWE>jlr^rs%1qF0*zNGVc{J{S=5BzO-#y2bi<^SX$HO!-J zHygM%P(u_C1quBAVFh+ZPa~X#lzCTr*?QM=tq+YxiWL`SB!n{rUc?WcOfZ+CCkM2X)l)}H2BF!Ra2PJS zwJk}Li7R>=4-fOAR-2G`E>;Yy76^lP{jKqZeObh9^<>*hFbmBDy&(wP#;?H{=I`u7 zE_jiQ|N5jBvKsTT_s?X={%pXB|MdS>T=H)xwg0M-`up&gME|Xjikhjn_{D$#GRqg| zi?AWypcI5aqlXN%0(K0{cDIg3ehF25b@S;RVd)S@wA6=Uo3>YXY(%Hb3gvlX4IaWV zisSxx!|Kh`9WWQdV~9isucpWm6$?K|PHLr~-m1QM!CCLIs!44-ZN5rTs7d+DQ!a8b zO164&2^EH1dyLn0E3&c|7UNpRF^hL`e={(>Ciu&6qT!doHPUo?+b825mJc})v%XHb zd)?FW39U!xG01RqV5cYhf31lv_rSL3fCB&&5&lzf{dfLOX2#aW^nY8%e^U2_=2Yy) zFlsOQ$v4BU4%wricmJk90g3H}&PJUPI%iL@*nwLp^T4E;YjB^yUX}-?JOQK!<*XLG z=o|p-Y+mXYBbY(cDZavF916bSp}<((xsE@e9xZ0Za0{U`=# zLJe!zErYXh?Zc_wHR|7r*v;9kLlT0X0I%X}d)Wf>$4Q*vFiaX}?|Cx&<&9y!00a5) zqR3ZBFe;+*J^Nr!bs(?>q=q2U)P=R;yzM4 zGR82VsK-uXh&tHgg@qrJh--$*3bp(x7k;#1+v9%!cFtb={dLqhv)%P|F4xt$U^(Lk zLqo^x^IW>~c?{nx_WiYi-~0X%sog<(ijhXc-1RwOyZyDFyF)FYG1~KScDbVG{rI$eD z!vvsUYtms+AbHh~2T&|W&GI)8z15;VgFF=|8iwYoR{%Ds`EgP7xcP<;WO+%_*;iE& zN~t#`n`=z(DM~=q>sNFTt(;y#A2&GzXca`7w=pqg_ea2^C%mnB)ZOt1 zW~B0DPw@a{@o_g~t$d#V#$ai6gQy%|ZBT>Bm6S{9U3lcl--g?h9&vTbUOi)iamL@xjAEH7`;b08J7&2nc?x^TE-!)LkMhgp6U=@vsg zy*cNS{Z!%brnMY!rKb6@ZMHGh`uouHdg)bl_u2IK!2IhwZ4-b+I7NUdAV{_{**@rM zd)4k7=<1Di1*Y1Q7+w2daQ+fuaZq=oPRBCEE5Ame^1|(=Sfp~}130UAbNSJWrnIv3 z3;#NV?9ag*105^4g*6@gqR!GzKC@@^i|<1Cimg4@tr?wXr3cAB{g0hb6xdj;9IA8; z8IMB5g$Q{rHLZvrMd?hEa$^o+6s&^`%;a0}L@SaKjlC>%Fth{ee)>WIm3oNDlgR4( zedZaG7Bu(GhT+TFU@@jPnHuyxEp3=i*7Hg|lVUYfC3v+*&FTSEcy2zo0*EC{Z#K0o zf}7F|g=?!azOEQ7&+oewk&X6Em39wLuFd5xCRKg<51qIKfm8(Ev_IM7c|d*-*j?jE zVF2;AKXbpzXo(5jDst1{+g^WEZ8=r))v{93f(NctDB9=kEmxMYPX4|E<7>4`GMpJ! zfEx!%;eb~?SLw6%@Rq-;235c|>g9zZ>Hdn{cg{AhA%z(qKMiDZ!sOU$x#pw@aNez= zpa2&=^}ys%{7wYMYU~S78cpbfmtwahTmmslNZ&Fl;1Mkn(f6m21QyTC3xgh)v0%K( zsLt`ZxgTP`@kx1d)Hn0Qu2tnwH2zM#GV?S|C09IrExfDW`v$O>8fS-!bZ_VqF?Ax& z>1VS0&{fj1mn?$HGBKGaMJ#?$4f@^Ok$k*=ci!;9nwtXZlr5lZQ^k@ymr2q)Vp#KJ zLZNf0*~v|uTj z)VA)2FGCe2!8OGMI|VAK8>u}S4{Rq9_qkC|r}bdxR8787tWEZ3#GQh9EdC~L8$%b~ zV29I%reY)PB&6S6q`S`(lDI{HT3x=BiQJHylt7!OVJewF?%}T-^%3>zLi^&AcyC#_go+QJJ}Q z9B=f03Dkc)p}Kc482I()6XE|G8~Iy3_HS&Y-(v6|HsYE8P4J{U2ALmZz?rHz+HB7+ z5Wmt8)`YY!YyZ`=LiRAb&u|lR3U4WQhK)TUgvQnZ9DFc=LaF|O#jpF6r;Z~_hbjck zZo~mI^W&48jJzI!zP>@zk*K9EY?0k?l|BvkTrqw*>uA0KdAS%=DZ97L&t${7w&auT zXB+d9eZy?b0@$Kk5K%l(AeIg^1zMBO1>U_}F0nt8 zumvE#$BhxI-ASm1>C9F-`2t1k#&R zajpW1K_6d(i0#iXit7vK$A2nxM!CS)PQE_I^OfLe~_?+RA9sPb~9!)IO4kQTSb zIiF|b<0t9X^pM-F-lqs}+11Adm1^4Zuxy1{QMDS<&=bcR;o6YMC@%BRzZyxiCs7LD zqQ}$bb7zEx?`HxCZ;F8A z`=XK_ch4KpQN69X{`%N*r7}uA(~Fg!QI}JBT$wUye0@0np@;y|Pfq#n#g#30${+>j z%eWy%$OJ8$GOyzKfg`3jNcyp%y?k(C8mV!Y`7m^Q+)z?nSVhbqL0absjQ&M6bPQti zlxaHhtXiL{yT#>yoZE2j;c`A5j*NaVC|hzml$3kF0ZbqD8-_Z_O{^{eeh)lwH~cu% z7-3NLICUW+dlKb-!2zsO=0?)vQJ@9nykbf5<~(7qXZ|cEf4uDLw9dc7c$Pfes|b2z zYrq}dGbVQL7RaBDILUmFEB{V{Uz8l2Uf0Mi%v2dc^j+*NmjsGNH2+APAO^?#k2392 zh|tNjM^O|bhNw8U3)_p1!Yf{(nud54M8}Y7Lli>oqcwGSsOZgRBVHdJikW;ImLps&E$YlV~{iPz|YoT9GPXW5)*T8=TzCZt5Vl6;$V=uE-k6W zl(|k7qMEBLU5BH>D6%w}lZ}+G$ecuDqBZkO|JBImFIRH>9=Wy+*cUxmeEhEX_4?T4 zdhCD^Y(p1!3Xkpcg~m-T+e&dILA0|WfMYvTKH;oCWvViD(mwwEI~e=<#O8?U;U;-V zMo@s>L@J~TaOZNtfqy-Tbhn>2`0eLfi#4{AXVr$1ENkhSwupg1P<=3FE^6Id5uH|{ zZ6^FUnu8JF>eC`yQ+cuFez|_d@Vf$;0PRiU7AgNBPjxP1)CKrPj?Bog2rH~1Pfmqu zWYK7q12l^o{k;l4%RO;lWe ztX2xAkHron;jPV_5YS_$Rd)uwWKJd0P7r*jdyIm2fu`O(!VW@Vxqx?<_3nOQ5E7mG zA($F9U&!{asN*oc+A8^;-iXwnA!y$OnsTTiVVdBdK5@R(7HoFx+k}1RiJhpv3&2z; zX#6s-RIIS-`eKyTtlO){2yAouhn+yD+Se4y^6B_Qs;qNCc~()GR|saGnrTMY4;-IB z3Xb}pum|hnZRG|kFTyB|SI&~iOY+fEC#$Zt(jsz-O?2k^{vFGt-dP6E>qR~)ZD|)f z;id!L87wfBC;>Pu9dTypvEe}WzHs@jI_JBNG3a%|XC6P+`0n6HdC~n6vM=xyZF#0# z9w47#@3yQqyG=*29`TXx_S!Ge3&lqDyCd}M>PE9*4{15IZfM*^v9ESPmiveb+nm~x zd-b%zV0E*8$kzJ29UOWgpM*On3yd?L|G3@k+vRL17TI!p$}h zNKvzsBwCV(Vv_cA-P()B&9ghCT&;#A$h&e}dkD|D2Mxs_>z6 z{t_Y5GAxh>6VJ%oEl$6bvOlZ8$I{Qc!@&kGa*Oq6JGzVe{XlE)pyI;)QEk_PATHEq zh^v>h8PdimNrbDFzuoR7VIY0|OB@CKMhNRKVyc#0?>%g?n*diPjzB}mxu6F&?W$$! zeL^LB5G}>2wqk;T5LQpD&7fwuOG;LSyew5NU(#+p7?kF7D}99&6t8m%Nh>o+k@tcz z#8W?7$x7`TFer535bG0Lpvm9fc3ri1tS(QrSB#Mf>_e&cBDM&8`69!CUat;w1PhU=rqLQK^qA6;e(N&dk%)-A-%>x9ydg*VrP*}AUDmY>obA6i?TT{pjHc&)I)HEoS?wB zF@(Ipp?g~MS&{CoS$?5wu4W*wA{maJwOtUxnvK|mg+ml@>Rgx-w_Qfg9j2r@XKquP z(Y-sZ#_RfJdXyTEFi8gLn?^-$hNGdUvHuF4Zp+ohHlT<)gEN_wC=p?l%sYr!JY;FX zZX4FO5p=XGy(f6pm}JY+3yEA#-RHJ~k{gTNC(?mw*KD&(iMzyWd@u{UrMgIn4xw=T zA-6c`-)Dtz(Iej|*yRb-IEM2GQbI;lH%N2J;jQc-PBCtDr4$)xA2~a#1K5ONI5_nA z%Ye-@i$5H6Yv2{66H=6i4xVtI;%&K zhdu^d8x+7nJw*DFi94!avD8JMoRBXyNRJTmYO=WtiI+!luwoEwL8xi4(9sDZM%`1X zSio-yXCR(i4q#8qvD0Di)g1A5WZ-vm7~e^gf+d8IU!)#;x}t%aw6fGKfig@=N7wJQ zkC<)f7?(sSl#VwuRTKv~l}*SSu2uvQ0XI5jQ{iuXb4 z%JID4neHs6UwN5|20X5;yzOe`dd&0CvQ?|&VH@;P#AZ{DuA)w|hD6%WDR32anGUCQ zYl_}Kf_}>R5^A*yyyc7^mS)JF4$;!@$)V|XeXDEwp>FPCn3J!xa-M^ zC@mSHELO)sxsx{?&Ow zU29b5p@F0q*qYk?bmKN+kWhZb(^A{Np$f^EjOb^&hyF#I)fP3BN^F1TqrnV*8_f6n zn4sq>bOoobLq4v`!I84&Sc>H>iY`zb*^L*F@qXTuctE&ZI3Jwd$0%4)HlBm#(CA6I* zxKuMSXVt*zJW_=MsW{^Gk|baY+H{8}Qi4d8RBYmh;-X|2-}YBid})W;h~hoao?x{^ zy#`GS-CYpRT_?m4G2EXb*@MsWx8o6ct!EE0q*z0t-WVRSxJ#u^)L0f&bM;XCp6RN#yQu`Q2P=ko z29$xbD66|aKdk0Y84jUcGHkY0uB2;ud{k-Su{BgnapaAiAZolYw0}#AXUYe>fvw`o z$xlR-SR+g)r4Vk-u?Ld*tcIfJ^p{lOa$q>QOPbL^D)oi9084|ya|tAq94zMqtJy$P z<#NoHgk9ZbB}CL+3+^t9N6h%(MmA;xqZ(hnebH|iF$UuE1teaR&2w=DokADIS1O)> zTxowXMgWh9Kir+|zV<=;u1*@Oq~u9;!ZoE?$81d4m1OzoP3Dz$q*d?6@}5TS=U&SQ zTzI?8QJ+(6w&^Rj9pU+7+sx{Tg713j?}?91uZif*;o~;Y>!Wl~%Crqh>-b;1jZ12C zNW8Tkv|bQZAuFLZIvEOI-T&wmm<1nVy@5QDp_x&gnE_jP6(=j$q&L~XS-Qil9GTw0 zY2zq1<)7le`S_C~`CjA-i=r6RRB98HqVUL|M0xh^Tj={8%|oOLYzitJ%2N27JpS7H zzyvrBo9wgdR_rOz`bUYOVL?U+Bi;{{^G^z9ao4rbMnkeqKBFq2Z&@_v-72rvlhm?? zCi~Xue(>5<{^`T>Tu@17osY{k2^aaiGA=dH|Ln(?RINzu6z`oECr5rgFaN0;V$0nh zId5){@*2A_ZJFdn>)Uu!_ck>@Gczw+AI0Hbm~*mHFDx!Tr8m4k?QU*DxIx2qlT~54 zjM(-7(T#hP7yYO`S!##rU7ix+tPM`cD2R=;1;0Mz3F0HFMv zs9~n>U~Hu1*yuju6qx+|)FZ zwsOOhnVD^Ih-fMhp?M)^3nd*=Co3Bv8}IKe%;-8y1zBQ7IW5PebtLwz=&Yzbt5l=+ zdhU1L&0h4s>mJ&?7)-~JsJ&22a;;1A)baRwT)+Xzzo8nqpeKS6psBJ(f9~@j)S04ut4ekDl zOk1@WIO|$$dPXYx9lWj1SJ2$q!6)o@?AmK^$~wAlK2ZxeI=n40Sx3r~tw{M$;1JNk zx9HLOI&Yhi;yikHou2XRQ0|br+Ct)n?eAVCKu^pkzEMfj8KJMQ;itxztUp zOljb)yFt47s|hC-F~C}9DxIe@Gn5{}tPd>L4aK~eW4S>eGC$Ub+Pnwp5d4ARI@h3F zKcon>g>Es?sBmon0Z0q&*)@*Y{UU5d{e9*ok8JWgt8Y?8lph zx?pwCkRBqQB4y_)iaE$PO4MsW*a3cM54H^Kuh*NVkV}S+qtLeR{?6emYqy^?0 zuCw&ATw`_Yrz%kRr~E1pkV(zk_HL@KF6JAPOA2vK@q`v4Psv%ru$0{Z2;o= z1^hRevp~-c);N(vS6!|39(|P%?CHeta4?JXhauPTbw&XEqr{L7z%XW#GL6|&c0H?) z+nz=?f`g+~(tIHBO&y)WI30gV0SdvcTEbHrQO0@?jBI6r-T-Trk^C8-fMc$PCEq9l ze^MerO_e*{i7#RXWxH^7f~QSCaEN{YO;>%LG&-A(BkoHFnP4iGvVFSE1_j155HBGd zLIX`_!C>5@)1sGqVeW5I-7pN}@ZZtZ0W^JZn=+H1UBkS(aYv6;dd^0FWl6Eu!Bz?5 zX=WilgWgUY$MzL;q5?mhw637JKIyU%5G-Gc098Ui4&uCCiwxk0%P%KlDl&;@Pbp<^T}sHfu0R5;J#o@VjCby6lS&hl3K0cb&f861|9tSd2?=GY_CMQY(Q z+-51Mf!LKncj_t+^aq2`9nfjXKF<*jNpXOrYMWJWgOI5x6TT1`X;t)k z3Zyydi8{~o5y_BV0<@i1>7^uY1(Olh+ANE;grmuxzOty9f+DkHRBO_9BNP8l&2ok{ zLut@en{1}JA!h`1QanWEvl3JRo%9b+$~D{hRZy*Ubgk;~G`>t=0P8vy z`vLN#lIQI&k|I`MK&)BLBu}P6_G9~FAdpL=6VNJo(T>Vt6#?S>RrCq9iiQs~tDZaI zS+c5$%(1+xzTO*Mm2&dPFu9cGxwexn*+Fuk4E!nphH@TIj34b}Bt^9fE2f9=&9!!5#2T)o79wKLxU(>uCqtA6Gqw zhYDNp1o2%eDuz+zRhlyv5ya_93gTfT#MC|L1rKn0yK4=ka7rdYnXtmb4FLt?(iRVH zDzB9SY}8+J2S_dWmYKuc&IpUBthdkb0O^x&p3It^Y4_BOS7xqv|1}~!b{RT}v0ne* zjV#d*$Ir<6RX1;{lLFxJzgd{p4dLBHqBPR0P$a}|UYOz+(y_q-Z5dL8CXF3se?i{S zAEy)!M-j`fa%wtC==xdKid&bvb3M85oE{P3wKK>4aVb}yU7Ji?4iyy9>66J>dp&ei zNN9ffywJYuwl(wh#@vJCMi=hsb}~QESg^o$=4W?46dNxb6|To8PVq>M|KcdMv{;Qe zBs04YjnFt{t&3(q44e1U$xkfb;|wq9Owy(=C)>5z3|l8Q{oEbSoZ217N@CN1DN}BO z0ZCDf*!rcHOltjHC0}r2c$+3u4TplAR4V$KfTkPAW~8l%m19*7S7H6$b~m!ST-h9r zR8^Cl5<%~pa2{D>zaz5bT6~K_Lz(UJ`6tv-#SNV4p`PF%XGp%ijw-N0wORh<+@+R> zck@fhEiEZ}w7@lHKNXfQiAD{rOWslrL1@SLB#6S{0!xdejL3FQl9x+pe)fKMQIwGc@)Xvq#YP?P6`$j;&lA2&iJ>l0QE>d&m@Q>Vi z>*ScK$`uqQV^^aBz6U_gW-p2fo z_VjP{Hi^9duj_3pc)!d4uZ+O-KN+Fk@|&*ppCwRS9Snq7)6tA=&UE;B$B$0^^OEK( zJMT}nMu|iR|A>xH-=ZV<{~|iZ+Fkt*(Qy^Nmi_}XF;P7%Rb8W+6H!ZD_%xL99DOpz zxm?^z7GSXkc{U$VKCP$C-+0lUZSh?Jg(vNtLDRI*1kAcc^*KU!hr@#x(PK*TV${Eg zCQ;{IAYRm)C%OU>0al&can!lA9nYVh&kny5`s?2+uRqI_vj+Zel-GZiMEx(GK=`e^ z;*DJrCVeZfNTbcpeEi8P4Q2mbdFB02pzU(vFR^(YZ8M(<0V`A@vVxAHA)4~ z_M>9P>|I*8jqH=4?Xs$>YRL|wsm8xr!n~n28tG8Yrz|LHy7ad<=~vus)8)Bewg>)W zgez(19NMP4jlhf=)Gwn{lW@H4h#uqrGD6-zMj-o_5%4=kHqL6r^CmgcI?nd~EmjXX zVV3#!`wA$>|FM?)Uw2=Ws$*9u`b{mNe&CBb+#2vQtcy~cE>TdcP@OBmKPBzk!vMoT zCkt;>`SN7#1-33FD^lZa;JM0*HPa0_C>6@dH`7rqrzDz$7!pShUn?3C3i5q>Fh$c> zUTa$&Os8}T(sK^xtM+*}crhFwu`F#C3FW4)fVisKr01@cbm5AnHhgU7*k4;ta<`^7 z90#xclghQV7d3*~f=^1C7ykM^@08-3W~5;fc_cl2Fy-ExwAgd6aaNvCI2@^1sA4bM!;{&s0}I8B zacf!$=}T75+*Evze01{cdH0X21cQ0%2f*piRjHfU;z8xoAy38|0mdF=zy%e$+zDB~ z?*eu=9N2>yFzkE!KKqU}ZEgRWoX==Y0JV>|x6D^xrODKoO*m5HoZg#tZtU|DL%Zhtb~d>0#kn%x6?RfhbL-EPjkz+9V(s-E5a1+f)G*t=%=CO#EAbEj z$+mt9_ll~q9jOonZ$Grm6)PRm_BzCpqA>zSjb=?UG9mZVD#$`^-o;${DOKM5i0L1P z#?)`8vXqH+LYcA);j%%kL^y@2L6rUdagAJOt`%XbB2HAm=@ZDX4xGqqmZP%7Bt8Vn z6xwfF>0y>4%6c&b{)+SXDFgIoyRAf@c^QSaPD-8~rFSkLopOedOly5QT|;CP10cbWW;K3?o2X=w5ZS{5aSIN624A zw4bA1ejIH4F^Go~#IIt?96(0_EjY4}nU%<%fF5?ZSNzYPqc_C+7(Df$VhbYK(-h#V z5{@o$!^S{7TkAMxgKA>a3}hg5M;oA~xJaUFESUVUPsa|ThkfC4(I!9kZ*sV&!=aAI zRX7RgS}>mXetd#L2KwRi%?~4ifV}weOFUz8r7j@)%0lX_!uCO@jpo=5j2`~T_yok;UtHguH2KEH@_eP2pT zBN{<(uJ;TFg$4*{C6WZlDg%QaAO;B9QV5P?y_o_fUEX1NB1Km6?pb# z>5$5%56HQGB!$}&6uuKx?}_{PCNcd7Ag4;x2C&P9@JSuTOB(0boT7KFKVwahiVk7_ z{n6t}*Pfq{_B;X2==gk#F&?X5Zm=7jwX4qYc4_`&<|@Or>f`IR5q;_UwD$Mm!0e3G zB&)gQYvXIRB#PYsJvHg2>f!pz<>~gRG2Ofck?^R=NxbkOFTcgewGg54%4rX|Mo^S2wM1je9%Qx&`w z^UXp%@ z=PI&=8spV1KXRJjO_9_8GJ8Dcl)E3o5$OR#e zhx0P!kKqj9Ag5&@vP?Mqm9qLNrza0iwa-cv20DU3Q$x_zLt1Bt!S3DW){mDP#=$fP zS_44ULS|OHTxGo4eHX872m}`R_c>H^Nm3$^29bjCPLvZNiE#FChlhO6ZQ{kZR&ogH2ktZ@^HD9TYNE2A7Hk`iZ2 zmMK3R^i6Y-ASt~^t;QHXQ-++n1InI8|=rOdph1xb2H6B@AZ= zwSlhdm~NK;sIQrN40?2_x4v;G8YN6_G|0H?#eaHT%*e-I;K)9cu>3JCp65#eh0IXUrKAnF)kZwv*Zgw#w*p5#KSH2r76?q^69Cp zO`%Ux5pTjp^LF1lC&`jO8A-l_1P1%QD0r5Q-potZs+r{*=R)bbTYZYfWxU@!jaIt` zA0xF)TAQbOr?Jg5hE3Vk!HLSEj0Oa4d*fjl4?`r1 z$89aJ{t@ui6*hZS%{N0?R*9rujQXlX!B6hGKL?-KDgMmj>|`#4=3YMuU z!z0iL@twLk*hBp?Dp_rC+Pv{UX%twLv|P(*)Pzg1)=~gS$wf4Zx@KkOw`#A)RE=V# zVHl*)z{OyjuBt6ZnqCM5YEBnw86I#v+7;Je|o*`{dy3@hEU(0;JH|H%M=6$ z*&O?D$ub4>HJkqWq)CyKcxZg)W*vld@Z*UXuj_?4vJEEcp_0c_&dJ>)D0)2Hsh?7< zFt|)lB%MHLU9i(TUS_cdW3zXg9gx0{o4H<74b)yfc;p^Kxo zhjPob7&$d2TF4il< zXr$b@p87mlX}t-czeDN&!am16xr`Dp91(xPPD#pCO+YT+r5Pu|k%khp36?@%qrH9W^Rs~u)8pJl z@O9Zzg#+q&7hjUW@w2Y^AqW6LrgCXneX0{WYBTs!AFR?-ypkcNfM!vejR8Z_GjhPX zH6b#2M~oJWPrOQgQfi^Ci@U8+=1$8gV{K&5U^tqWfu3Da;%h?+*h06)HM-uqP#BC> z7DKFBq&ix`YK*gF+c&X@v~@VW8oq6AXZGddA+invvnOq9>x&FlR**W!_z9R?<-J>y zqpNLKLUfCAg0A86x3zncGnlVM2I`#SgGr0_{F_FD=G>Z@;BnNTv4n(hB&x($pP{Z4 zZN{bQXoL+|uKjN^usnOlEAr!16!Is_Xzq0{bV+;qQz(W!TYmOoZWr(sXD~#X!y3L> zPRyrU37TL(|A_MPQpx+t$@q*N4T!rPtJQ)WiGz<*PN_$m*>N>p=5QAO2Ql|u&nr7)lVHA zAHF_5-cN`3+h6VN9-m7SheAIFnO^Si2axHyfZV7_axaz+Vn?cVAe$bOf>PfP$ zpvzz%N2@EF6#BA(COW9F=J_1k%4g`)_Hm8&^50=bme-5?z(SDAi%cFJ0-?S#Y^kTw z*4=|sj&h3ngW(M1Pl(POVr@MaXS6sdJ}5pc4hu1%M(7Jy)yenE)a;m{wbk6#b(-qx z1GY?lTE1_U2-Wbv8kWbAiwF0PSN>J6H}>YzUJjLN6|@1ws0~jy^R`jQ*JkAB)Qg-B zm4(NL#Ye?sA*jMvXK~fto^t;5qBxR&n$|$%9ZcZNPh%A2x`$2kBh}_}ODKFGX1Rk7 z3ufSre3LiEry0SAt%zL~qL@RI*q@?^HVv+HU~%s={C>6w(n!)8KUud6T5E);uNd$I zIcLvKJ26SYBdlY^t{n8;`>atcxG)PdxCiKv)1v8w_%YdzX;8sf!L*=UAOea55$kOk z`L@n7`xeGTas(DP-3AD75mAy*lu;H@tSMIsA(Hb1x2^qAgwH2%CWP229gLCEXv6H` zWxUNd(1S=zM=G}^p1^msqcU*GUQe{WU2^)L@Spg%;)Rrspt zaZ^f^edBCZ;|}Jz=a_1b`Hz_yj=>VP*RUT@>^RE`kfRgE{#U zjr|?kpb12@2r=S>n4m^MW?7EPTpFh-o_MdR)?#0-ywa`hZtL=aT`}~3!wgu$>B7d$ zh=#78(wSs_?F?=Bz6E#EkW+czq_?z>V8_0kAH=sr0j!|NpoyRhpjO{+&d<=u+Z=s4 zi!D58;sm0RTNJP4!h>bisR7WO19LEhZdQHOJPj)H$?KB7APtlo`I%wlrjk};EADVX z!4)is17RK)8LSPH@(6ZjPEvbktKGu@?uO|Kz-eXvO}}J zmV%#xiDBi_trDnT$s!j8+;x0^VGkuzPQ^aW1{)3dOe-{WX# z-rs?|{bq;$*bw?N6N_#o&uuyFF_pfMWrJdtn6Z9?w*SLvXO(EGbf)5P2sLZ;5x3DN zrqN3e<^v}fw?7k@uXXbP*~h0z{f#X2DoaO;f=6iSDqRmAD6%a#Q{ zy_m(pu$Q92HA`!sfO5PfzMbLFxh5u!FVUN9$D_0O+W2}s+0|7c zJqiCONi=qw^5r!7W%Syq(9L7H=zOpiV3CoH#$7qjBvDa7W6!(nv->>@fS(zuN>m%X zbJ>}Q%!>FEV*t2QkhLD>Fg0W3^}ipoN879F&Ggr>?#Rp-5{qoMLz>>T;frjwMYCg% zup>9CzsQe0FPJewkD*1%S_P^reP=dftOwVho_;Iyeb(HOobs_|Kq4c_=GBsLDGQI9%g z(OR3+=^n`j>AQD7(>gvbDy5!T|J=Zz!b0?}<6z{jRSI3{h|7#LdnYw%&eyCTy0X^T z2vvmNh09Kp+UmDj?*9x0JUDQ1uNysqAN=5%0Gx?%lw~GcS?Y~DHWU<;q?$3PbDw1M zP>EnuANmEM#$SNeuieTC@{HtWyuD%WKQhuAZiP`9e%ELhgpXu zgBCSLB6aqxaH=L4;Hu)-JL}NgqOa}pYDpIrvy*4126@zSRy?vTvBO5);rymK%NMJl zQpXGvOC%;a0D>$e_c9}SQ!DlKnU8ml*8<+>2~CRxu@0$#Q)MpozBhHZ%;EI4b-NFz z~JA*VZA5IyK6h$o)0U74a9U6l9HR~}MVWLbK z+CaM)zkr4$xSwhzD19t_ngPyn0;pl2fL~w6Y{i#Gis($`*y)SO(xu3R=i-Io`vGN$!)S zH~=(JDg0br1sfHBT?sG-eqI3jZ%ig;f2#$ zW>(|h<%p52t`39^XL|Y){OIk8M!-&M)a6%Ky1iEV-7-x+{4yni^7cb(O3OteYQm^j_5KJRs$SZ~KzK(Soanrgel zZy#JR+2;HJ^faEPY}Mxa3{P~74(QD-klLT(m2tufXqI$Z+*DN;S}f8!Qo!t@I4n&* zDD`=5sU^THn@dktrSRg&+e;*Mx~#m31E96F>$ZJPC2y0xPZrBW%zB!o0yk*%4Mmga9Yal!3_ zsW-3ClnhXOy@ZP&Mu zH3H>G+d~+?w*#eoVOlfoTio1v*EZqOKTQ?DSotWvHgT@!tlKIzO$R zqlJECyOcU@UAscZ?>6&<0&lACpeCTi!N4y2{>N>~)v=ZW$_FO+OM1YrJl6N*)Z^@V zN-R{!Qha(6^Wyz>>Ext@cMFI95dG-N6di2O+T~^aW_=>yCd(0YOU|8cq+!hy4=;D0??WM1Hz%um6J_$(HE~N? z?&wR3V}HO+(i9vpl}?#s{x_B$tT=0m4XndB@@0uA`Y=>2@?{4S!_Gh*gBWRsCMAz% zsH7N<25eP64W?)y^3F9-%X_?fM*w3ppZov^4Eoxme^DF2>Dbgtjb5Qs=8(S}90ctQ zwQIm(dp%xU4Xj8r;0%1weoxk;5NgW-m^bcgi-nh*q*b#>sC{$=^r77~4#i$=XLa#B ze~!(~?WJySTs;?hR+`ag*qJ@t4&8345uuaGA07n0?4)yt=kjWQJ)8_YY&V|y)_lFc z-@I@&nnY1Or=D0z%A*fRK9`6L3sj(VJGN zqY*hGx~*ca=HTTr_@CRWtAucu<`D>*!4UKVZZ@}lEE@DJf^d>o9TXjqyv7W-+iM~c z^O5BPEg&$dfTJ<<<=kNH8@&6+8pczT;V3qK)g@vx`kH0~15bpkgw{Q78uM#?UXS`N z(&cj7-FJ%BZcpaq;`P+e6Sl!*S0@K}IoT{j6%f2Wx z>ru*vr051(5N`vlz_BonE*d-J?n>eDwwU_3CvrDECKY^&p5|jgeu*~?dX}g{p!Ets zmMF;wG=>LwCB3fv_1rBf)g#Ch&jgb~_FAfbQ>T&*+B3DzPLp(Cx+h{ygLA015Q+a8 za?@4gkxhR_tCpR{*9ced_p;Y@NPl-A-vO$NPKhuw7XF_+rH(2KQD<3GAZfjb3P6&9e?xVsWpusfilYbBt-92hz%1Dr6YBKs*iKf#;U)+m00oh=vmw3 zKr-;W)ZZ>n3V!epjDOq#HO@=JNtd>2LLpWzRLusWij*P0bR-!_zhQ`}9qtt<>6N ziDi<_i;eoe@pV=n=HT>TB^TTTe&!jw$aGA5+9WLJV7(~WB$W6vTL zB+9|5l2OVVelY_@L)W5LFf^Kbd-&P%P6MvOSyw8%h<)WPl(Clt5E2^9jd5&oQTI#b ztq5b)t%chJrPl9q4>hBOn)29S%QILj}8$-(N-@H0r&1Xuh_TpHaZA)#v;CSX) z_u;Yv;Wk_XWNIDZ0yRgOw0+@H$=tGazBO4#*}~yhI$R>$QUfx-SmL)kOqzk`Pp$or zYiJh$o3_Js3+^@W#};e5vL`6}5Om^bn??R`v&=E;kOD2DTq%cyamW+HsQH112L6vT zMVPh6#1(na9JFVXp3p*RzSszJ&65uSbHh65huJvziEG-d{C8e_KrdIcfdi1bUtRMs z#nT0i)YAuP`1>>g-3ZH#M~A&$TQ`OG>R>nj=Hapng5pcM9`Q9irDU#syicCks@Ma<5mr9`e_`sO z(+HnI!CZcXbzEQxuVOJ&M3VQwwyl}=E3Ic2I%n_TfMrZO)VJ$}oD7EaYWB9#B#QA^ ziRdxbDT`3NUt9GViLU|2f>e-l!3f4XgXKxDaJ^Rn6L(3RDCXi1e$|fc20kZkuI^M5 zAFascJsBHt)G?7W6E0&YK-T$dcw5LZYeAu)tXd$v*|IX!^Odre%UR5yq$Gd(Z#@Dk zgi(1DO!#8GLv?gzuK(E&LK;Ec%9!ivZf_9$1|? znubF__-2b3%tE0{qBy6wF%9WIKz$bY<#>9WJ?U)jL{1d*OupnK@KAt8?$$U~lTGG@ zZ-Gq%{>#7_#=LYqIAGOg{D=4eN431a^liZv^lWL`m{I;KP{!(06@4uTEK!T%1D0mH zo%M|Ln%T0}vq31aIEy`#oj`)neR=M$T+I6+%lx~WjKBf7qSD!4$DVpsJv|~a@N9YzM$MndKW7PkM?3VNwd#Qb4E*;G5_6W&VwJ1d(W z!2l(@>tu?i5tmOxEDm18jUa&f@fI-*x5A^giMRc~)5&bV5OQeKmfiO1*~YCX zIVayNx2%=*z_H=QrfOxbBf5y0=} zOi^_0b%*4Ohfh===d)1;1&{gmIp7>Za4 zLnA7#U*9MQWf-5hh@lvRaL5>wIV~a0JxZM_l;3u$l}DE3kw>V0Y1aA0ZDp}9j=Q-e zFyERkM5*Y=Lks$%s?m71`?;@ppZmmnbQoz^QIy1#3CzI0Z2UYt19m*I-10>-q9)J~OQlE?Baie546AtfPB@H=x zppN-j5Eq@MpR#$>PrVu2s*x|SjQ{mM@$ca?QjObG5Z^VBNaX+U@F;+coB>XDHtIIk z^cGGgHvcMrBq$&OqR1oo_V@qi<2*wHK(vM!+>^`1SPK;k@BwEYkYf^(O^=}+Rd~t0hqbueR#(0 z1q`0=4tYlUgrRW4kCNxZc`5?^Ga%K|=g{tbx^~c_zCa`fP3pBO{chm~QYodqW+nm^ z@HtM~N^>%MlpbF591Qt`izcn!co>@6XD*R^!^iC@_a%FD{V1Nr0at`l(Nro1RN{lI zzdd?tFD1t;t^I4p!SRx(B z=g;EmypgD$>v#fb0Dy7&F0AKPDfcI#%V2uEsZ<0DD;tVNU0#10Z|yz?BbiJD72fbZ zhB3AR= z?q><)0x3U2Fu{Ed1rxCl6j~gr=eGQ*EFvSzXH18I1VUuC^HnH0$jk5@gTXjj*G2Wj zLV+w`4J@2h#^R{ZYSk9$ED(oNYl#Fi89xT&Kdr{4MSXYgHqQ|I`@z=rN=aBOgH^kx(VHyN@J>3H!m!izuEEsz$U`HM zNGcNu!T9q&#^Ks~6)+>avnO$bLJ|^fAr=t4Ox&A9P;1S4il>`_;+{u*UMpUA0ETY? z(y2%w5^k12<4z(Q%cSxstT8t<kXJQ;&v!Nk2_G7`mr zb_X879cL^Ghp*G`MzLTz7Qy2!$O4gIFxJ?nPmZJPd`WGvn=!zu5kaGYFqkiq^edt1 z*nR|Ag{X6$jIavshM6pk@E{$@wI86&yjLg`k7$#T#4_NF#8S>oDvS~eDzz)j$kvq1 zqRT8AsaP^iE78IMDz4N!up6C!i*8q4(_dt36*NZ)`V$! zZ!<2aEBTbo=!rcNt3R7&OX79aj0A?$0wcadNISh5XH2gNrMuOsE z9%n)t!={{4e$oyBAR-y9+Y?zyIYB^PxKRH)dlVi>bk$4zgS*|Md@XX0a4>|2IKY?_ zVK)T}ui@OCXfh0zlS;EC=jim0Lk2DjoA+ z-#~U7dv5}(!SJ~`1@!8+SOj7yiN>q3ThXu|XDmzk31!Lz!20)`dWRt~da43Z2H(U; zNGSfyjGmZMK<71Q{#X{7=a5+-mOI+2Ybs-`G-L8SnM9Hp5sE~kB90fH7}#S~dL2T@ z1|yHo&|GeG(>0pELU-;}4{xo)kYcRXIL&s>NyqpvUp7rXkpr&CG%{>1>FeEMS{>t? z9nzV!pRP#^oiEpodkROtUnf}>4*)c+!mCJpyL~ZU_k)_HcQieJdLZN!DX`(Jhhj@R zpZJwt>?J=hejCh->5d~3%gAA{dK>IHjlAUf;#yO@y zCLIU`_lt_?hk@i>-~otLJrJuy!h+Y+s3Pe{LC0*Avs2m>Phth zZ#Y+Kr)V^KI6c0*yv~G}46jjb_`=eQ(uKt0j0Zd-kcxOJFxyU@z1rxZFYO1eSKEu@ z;AI%Or)=`B7MNp9^njKyC}hK-swAtIg5B!-pO2X*T+2;ZcAu6aOmXr*&r#r+MHr1G zV**F%pRG7w;=wt~SAMw&s7oM1H+!-V1D@Sg@=Ben9!bYi;3TZItS%K2pZM@cZlgF4 zZ<18Qhbv9$5)RsPUb(Tf)9ex8k?4`k3tzU{*;h&j{)}1~$`JbL1NLbJQ`x7;k*&Di zJTvxg-M-04z+6T4ZTb8G^}7urYG>Aa4H?WX`+SidcYv9;x;xo-+o$?JW5k_pH+%Xw z#tu0OTdbSMyjSm@O8H}vHyH4-sHwKRoIO1|9kp)m^2t^!+0XgoX&@#{I24BCgH5$> zIvlru-=4~GZagB?BDX0?A2aMyeXBe=vs%xbUtO!#Th9(|H%89h@!09?u)ZFP?s(_- zuq$@Wo_KI?PTwA!K43DB+U%bx@cZ2UT*oQpOycjo72Vn3#N2k=JPP0FB5iB=Fq@6t z7Ecf#ZOdWK?IE?Mpu?uRM~s|#RnXJWX>V2FzKonovFA1)Jg9H|+F^F^Z;ZWWNw@^; z?P=wF&U~k4+|zqb@&H|*Yj<33TTYgLCHcT8D0gvHOwaKq1-ch0`Mwa@zuuiJ{X*_8Ccts}Z0K0)lB@M-Vx@bKa}@Aca`@WtDV*YU=V zLk=MhuB^P`Eh?329-Oy2^yFux7JClN1P@*h2K}YCYMO0D#Fvck?;A}bNhNO6J+8ei zs>9fk=KZ3`o>wn1kAcHw`@P>`M-5wr9{Ep`uq-;+_ilTP_> zK4|1K?RpvJ{|W~akklVpm|L3(v~<6-8vp-TF;mbJQu_H`l)O9EFQM+;kT)0 z;$rNdjXF0CBt;+MhngylWc0kz>{Jd4gWZ@Xac#2S9ypj~xDS=l)4E-}>uS5No`#nW z?zA^e9{?&`7e%$zHP2D0)zeM9{!7uYs{4bUk331+#&AgkT`^OHGfIRb8RR#mdT=VB zmFAU>Qob}j?H@>YFV$O+op2M{m8- zpR*5FQ}7Hvt}*SS?;v+alT)mEy{9ujFiYmqUA_vuzUr3`;?LgFw{5xRmk8 zV;HwfiaV`0aVJQxMl3Wc>Da%pz*nv^6|yguIa7{wHm}#*da`>WZqt{PN?&Il{Ieoo z$9GqjC$5k4qo35-(`)WdUXfOA+~j>A5m0jQE%XtY`#gA}i2;L2@VX3>+TA@5=~Yh; zKHar@mppvkxuwO=BV0`@cV=7l4|6pmR<9GM2oq1+jd&|hFWVZid;`tSJXN`ypH94I zx51x!^qG8)w~f(P(d<=t4&J>|d_(UOO1M5cJzrKnV;Z&E%Lh5s+lQ~uv8RtSUU_zJ zUfbPAYF>pPdvfo-wN58v^mSNM9)-iLhxi@cS79)!BR1PFpevtCzGmIqZ)D`Y1Iq{J zE3|y8_`6{}+PPl$s=m4RIXh!=7uEQ?kATTOt_!gj%Tj95A7`~|NB6i2W`?!0fe+}k zN=f4Jdm5WP5U7I2<$&7HLQiMy-(2DWF{eUjs;Q%KvuiD}gRclO7jJDi+0N-R_-Fib zbTONBe_pc6aJYM1EQ*eq1_EQ-vpPXV3I<2(zBfeXk?7D8_te%MV=4?=noFrZ1>M&J zAML^J`{*Ec<(-*ntx<|ZOM#twv#RLXPSE?T%4VwNW*`q7(C$jbGI2INe8!MJ(B*mi zft(YLxK9x6o#DUe;2VAs*``wJJEaUY$kpEhPEg}bR>Hq>YX^OrGgQnXbB0b?zkC@8 z3I2dleP%*uhrq{t*<3ot)lGSkQ&#zelVoL~qj6RPS5J2--&M+jjCa3Je)1XR!oMb4 zhGzf=T9!~G<{x#VsYh0{SW#x%S8#xnJL`wF`rGAbM|>6;Jx%L;wMQJln95!?RT$>I?*ACw2d!Y+7v0n?=#=@vnRpN3Y&hGlyE-`cy% zsH(cHO^0-+bT>*$h;&G&^r5>;8bP{4kPs;;LAs>7L68P11*D~$Z-eiB(ewJ^usdyjHuc2Km)_}{`^DnolA-g7L#%TJrJuKoc;1$wr#iqh z;j6O@MACPN9#lNs_BZcw@*CPadd0a9G7p%QL_ZGNlf*r-b;?wiTZhQ(m*fiR;TYUw zxIfCDo8>9cpY~RG*XFztD>&~*?9ohmZ1u%rspi{p?#LcGnq>xA`qv|#Wi}hS{nh7( zXzLnhK8EwV#nOla;v9V3Z(xko&-UaK{yaMop&2+PfT=eJI6cvSot+>%yZ=qjy9Y8g zPSJLn4ZUlgc8gELds54OT6IhGzPh45vnD)7()N@iYRUTKt1a3O!%iGrjviu|y5$QC?~PO+HlofooR z_*Nxwk$0rv&qYU*aVvzdm7w-ljWBL7Vzf1%ah4V)#Ilv`RYGC)sK(BsPl~fq$Kw@q zx2G+aL|$8JYEJFGG;MT5XUlr+s40b!z-d*V@K(`8+E!Jn2NV+dUfb96kYQUxxCbPS zr)1XM;A}K@{85b=eV> zNT15}#+w3AgplJu+fRmApSbQC$?25OR6WrtU+(OD3k`}VrgXsJ445iF7koqBC_rUH z@aPMw>4R^}yoAR5K&4dku2Fan_DWUE!Eq$pnL-||iwXTk?cJ%NAH)M)9ae)wgU15$ zaj+74qTjaB(@jsKdJYyhg_3@stz{B7N{9yh1F#L2b{_lDC<#`w$xXot1;E}OIcC;<&B1H&0l~EdJDU))f z8D1}k{7TI%fg|H!PG)VsVU<8Iki9RCr~`u%?W;^T?L8QGnYjg5YJOYpDPox1an~(L z5*Z%Kwz9g$2WT-+og8FgaYO=%CGzx4*+w5FCiViL&f?Aeq)aO-ee?kR2HdMwd#wUaN0SP>1w_v?PmMVdGzTR2jh6%phLok z2VPmW4>{EfG;j{Z=ZhW-1<2F_DJ_Gjt<(=MH{v3KuP|PBiV${-7)P432Xf1mjy%jT zrI>h$S)0sAc}+p-Bd#KT<)!X*8{Y+<4xPfrjgS~9@F)rtcm$l%H=8VWK*}&DV+Unp zC#Rd-Vc_pMSPaZeG?#0soi3z&gGM{qB;BPwq4g}ipnQc>Xi7sKtce6CEnR16VOe*4-Z;AYv43Kf z?WH|&CuDX_z;jQ(l%%fv2an7^-YpT{`wz*f+!WGw34-yM5Tno<0n((ApvTN<0#tiS=%K7wHcZ{H#|Y@ba8k%Q%&T|JBL5Xx+~>})?Xjg}ys z*39Je;5cfP{=+kjQ*Y6n8UMb;7yibcqh}@<6P@^$6mk0!ClsH}Wxqpm4u!?V!i@3nv~qFgZv1 zh1q<7wRH{&%NW5a!nx|UJ~5#5oO+REzlb;L^6I5ld~Qx^y;?)) z?kTL9aq5+%YH!Gg(gVx1A8)3=!GtOeE}Oc#-GT-u7#ccSRFeOSU>^QOFt#|V*jJ6V zJ5%MW+U{~Q(D9bJV!fAYVQ(FH4}~)B!Pc6IsSxJgL!=LBE{_O7Ya}3bYc{?11&-S; zhAl?nE0>$T%Lz_~uA*w+PU|r`dQ&-GUF@-il+;WYxr7c708?YDb^j7c-Tex-rbV?6 zrwQno`~{Jx$fDuqFL8XyI@!lLyS9C(6r}0C_~dN%_s^w44{NJ$v%Q6+fWH9w?A5%U z+&SY*Y9Zb>K&2lvz;(>Q5tIm(squ%e3BsOW^Xrpj59u+5fMdopSjaR-(if*BG{osK z&Nd^gBH(TBdt5K20aE208_NoOLcQ5bN2|>Afr{-OBXSBR3UgLyA3h^jsp?l{h%r-y zPrVw6@Zs?9;WIzy*oV3gmS77VFzu;}nywno992b#YaHhP$oG;~P_Y+aZ+KEphNQ8E zKYHFMx;7wrV0@5|dod#NILX#G!R=K@lCR_>D|C9R_))tkBxCH2`PBzi zksASW>+#cst9x@$N$Onau1{(}Y-TH~>Vw}R10=*S4q ze2?*?vrWUy0^Cx>4;q^lT$^uPsL57RC{->coUP+>H~d0=s{1E?nTgsJ%NtGZM&rff z+rd<~S5SK3c zEnTSLoSb&~%m`dCAELbDWY*Uw!}ep6Uv9<`g3i*9#&q$uc)c_qH1wmMW^iE;Nv(v( zj%&EBcnx-T?WsLiSQ6s+EM_BR!lFszB z#wM|&mXp@5+PEFNN?m4UKWQzpo)yy@nW5FLdx2_huSKCKSHogGM8F{b1NO>ld(7e5RX8*xnwE&cNBo+5|UN4(T?`Ct~Hh30A( zz}KfWuBvQrcw_f6kDIhYjz+{q>;Y9>X_l8KTS}0TRT7`1YGm*Oe__T+1^0nNj70(CK z$FI!bu%8O%2T&jnhkA~)vT|)6_bwXYm*PI*+3=U48(d$=zTa2p|8I4TTvw3 zGDj(mxGTe*6w{J+sQYGkOrC)-i6y=tYa(DK*h2aw*1q}m#cqxl=jddJ zm67`N*-F46c;X5;8~EU(ilXXV0YQ9QarguFjy(ogG0;npa%P6HiVMAA2TfI|C)C>w z#3vj|YTqE2AvgT!?4?rQCet$^qt7v(;e49PndEA)&cs#oGVnAYeTbc)Q)5HkG^bnJAnWWbu7=ZI#i|xlJg6L8a53@QYxNl zeYUN#xY1qUIP%D;3CDw7)^Q>%o_?&*@~BWhzg1&`+>#`C2GvgqqxPIhM(rCjxDQ{+ znF-T6OUtIM%vk&g*Lzebl{WfZ_WS*YLUSE-?BUvQH@+o@73(RNe1jC#QEQ9$B~SAc)Yndj_*UU%?46w~T>8pqpi>gjm;X{_=FZ$neyTD*Bu@Bm zRGFYOK$W3ZchMyT?vySgR!^ zgRMC518{ZoG4;MgMXxv1BZ&og#w-L9dQjrA2ns%RCajwl(H|3))L$&=dv{7UK@Ycz z_3L9Q^Z1u1wWvN_iQ)kiA3tau)Pz{pgu{uA*ySsFQ3>9 zR&tz{*>V`FX1Z$M5as7ro-fyS(s3BcoLTvpAfkf$07a%8P-G~DMDtY`{GLBQ=L?`3 z9nNVWW=u2e6w8IRH4F(W@8qq?EbSzHjgo$D_U+?=(>6n+b)j&?E_v_Z2G_{W#m36H zF~BwW2!4nD&w9e5B`njMMZYu&Xb!Ok>OmvO<8OrRCE6cZ5d=GQwU=~HYn*T%hicXw z0>#p1RcGC~vdT{>>a0c1LB<4o7v^-mhgnzVY3RYm*aJs@HnpWjuE|ONE23csCz_!f zqIrd0{2HcK1VIrdp>Ii;>>gyTnW&1AoJxU`)Qz=Ne`<2t92WJ;r#eM zr@54f15FL^3@7KmcqU~(sT~H@keHsu5LIu1X2lL^4plQrZ})ln>1%|2Z{tsBnLUpEMOxPa^-g$#9S+kD}IiFXSxzbuOzG!dE^;v#9Z zLn);Krjsg=S}&xSRv^oa%wFU%3|RGRfF4Bqxa(i@c~s-PfW z-PvDyGC`5$orbQI0=61H7d4@#I8GwFnw3FecP@lG36|(XLs=3wk`j_&rNmBpi`v!^ zh)*Bi>tUpAwO4(!Mmj86S6)^=56;M2ayNAM7bnLp8D@WXd>JyF7_&CCrc8hgQ{(da zM6ur(xtvE*UzEK8Q}uIJL4IkX^QZ#pIdAj3adU+YPNTo34}0I(!w% zw?vZ&5RD;!vpvaI7`+un{&w}}fXS4sJ3;i9$@HCSUL_^hbG3p7s2;+QyP)e#+*Vu`IlRJX|@*>S48YTuw}Ka??{_cLU0-ENt!vb)$+}*rR6~*$SW(@ z(lDGerstH>6Jx!CW{+~PWAJ0Us&-UiT{2)YDWspx`QDgJ1kai>?o6g4z+?iI8q+^J zXuiD1=C|}{#gCM+8ErnW`pQ7vl|739QbZ&J!RR?5d=bQY5=0mjE{R7k_VuHBw+A0W z%9DIrfAY*kLcQGEu!aP2+zq`h2K{eKSS~5V$mmz zOKPQ0?c^xdzej6dY}t3McD1QVqm(=P4Dt#q@o$Z$l~}NYXniXJYaNlb zo$8p~EM>Hj6N=4>_atoE@Q4s&X?Imlt+isT0?`z`b*o=_uaa`lvkGrqw+kV>-|2VI z4dB=Gf~5()+FEpP8BBy|#Svd?(ha{RgjQTk!pD71gw(X&lu2^s4lDsR~WN{})Z&Y9R5t^OQ|0qjr=}OrbBcG4WX&%3U4@dMe(O1@0^{`33kES?I z+m~P=Wk*r(csv>}T*_l&_%vMkg`8XdTt-F{iOX1MYZY2!_@3$cDse^ z{L2MH@l&@w!xrAvcm&cw3_lo20`ATWR+1K{Bx!4Ql9E&2% znURHOWyj->b4m}#GyAY`v$N3*DVFWR4O(%``qBCo&fXVIY26bu*D7Rj#puGLPCmXj zswhI%X;9|RNtjHb#@+hf9aWvvmeg;Mm;r|QWY7NlO<`U>yfH$>i2s0 z=fflC#YovqB0};~=tG1RtQ*+TF?$0Yq{SO2SIwETrn>zbXy?XlDZ&EKva`tIUb515 zzp%er?SJ+EtQ+KCGM`leW7-CcDd1nP;RJQyOE>v)0LL!A*`WalLmzBDCKH&%PY5Bzlat zMy5_!!J62I3aQ!WY>6)^_;6uCkI^@Z;BAVb$x$(UM^>t0FmiSE;LyCCn|0 z`GN4kNY`x4TuPORsnIU`N7*GliLyNuMKrQP3fl5RBUrY9}Vq6i4PpNChw7UkVdIgO>t$WDPg&5NLCsNaxYEN^%&2Z zw0<)Z6*CvcA(YYYC9kchXScjvd-bbt#vQy#PLs8X}H~!12-AjZMzv!>~U!`Uq zhYVf+(F_si7ct-kMtB$&0)pb_C>y&0j#L|v)l+jDOGnmU-`(y)r>gW>%m`u6;hmwi zZCVQ~@sySlkGz8wF~67p-JPFM%YT*Jq*MG0E@s`X4-dYuU7B-~q005sn0N+zrcc8W z!?O>MXB~H9Y<8Fyk^4hNR6-%#W$~aWN|5uDVC+r~4VST@tfLVy;$FbmJ>lTrX!07R z3?Y{$I|>Pl4U5SkVh$uY#O;!XB|Yz zpx-k5?SzwxIR877jed`z(OU2W3tV127-Cm=EOHz|cFg8>OFt@5<8k8SADZy6JRQ;& zyj-$OwUk|kp*_NVmkzIXvOQCc)f-IEUrr*0n5H5q zhbveXokE(W@pk}jJb~>vnP&PinmOv-`C+&_!lgxPbDgMyFans*&Hw4 zWTq?dw{0u9IQTd>sr6AB5Zy!^A$=YD@nHIkI-``&w-?nehdUp00yV$u;ygfilw!|+ z(Mb3DS}@^ZCLtpxr5|3xAkcIYI!~7a4f3web%Pm6-s-@oeum6-7Rs$^IUBe1IJQVR zjwDksdLJAqNJp&-frLq$?(|)#ihpk)#X8LU1HVaF%{Ba=mJBnvi2)>lUbunD$NZ;> z_p{LaKc?Spp5gkK7U?!2Aj09rH5>n0+3WAVs?xM#nh7snNS;9&?j<9?u1DN|7ki~t zef9LQ1Pm;aP7QjZtK+u7?5V+8J&+HZCE8Ewoe)_NvbGw1<(# z*F?O1yaTRUq1l=@9mCHJpk<{mpGvb3Z8Di=NDw}1LMbf|DMz=QnklQ~lrH+L(wUCj zEuUndLlC?n?N4_ka20sJN=to-A^syOeOi7r4^_C)WmeYF)H^s)kYPlRulC^AK=v|E zGhxq4D)nybdkxxLk(-i*GY>ZDA}U6t^&-X&HRK<+{jl?VOj6dezbor;4~MYkd>x8> zC`INC; zWHD29Eo>xTQjIN(6-f=glwvNoc#6)iRK%7PDu@VZ|3BwJ{0nT0abOPk0q*U;YU?JQ{{xi7)K+cX+pEn8GU3vVnlS^(J+p zWVv|RnKUoz^z$z<6HyJZU(<*f6_gQ7A<>Iv-UkROog8Vp(W7r96(@-f(5KeE>>5~= zx9s{tMcWWNtz?#JsorSY7szO0I_WUS_$*7Ye~KVE%T}xHnK98|TI>}+0;V2&28iOl z$|*N`NB?p0p|lFKk}G0g#iU}~?5i}~&Ff}87Dcde zzEkw=^Isy0KJ1_mUlAcoC@C^p%}p8JO%}#2F5`h%8fG_7|8X#~Af=4p%aN)&%fDCK8PHkI%W zWx+E<_{X2b6i8S{#W1()y>K_{-ALhd-!LcY*2#=`#n7A{fxK}lw%&_$3=CL6qWhE| zJ+7FPSqf>e0MV+)uGYd}H=EZ{%CUs@uzz_zK4lb*_RJ7Y(k z8)aQV=68VR;M{IV0ALv)Y5?Pxiw5v2{G^pzHUSqjP%9(#1O#V*h7kTD1_7Y~tO$UK z@Ke^+*1^)i*4FaoyZ?w|cm87`5csM};M;Fl@9x6BcZ>5Y0(c{k$)23*1A^jgKXX~! z33s9XE$nD!Z0zL7di!!)MjRZ)udX3C9S#CQ9S0mm2k05lm76B~io*G)J1Hr^>na6w zQ;{7F_)Iu*F}+yCZ%P|0{?QT>AGouV1Cd zqJNRTi~0fw01x%KgTRWr9fJQP@CO_ee0%>6)G6^l0lnSq2L}b;(7OYjNcm4dZ@2fr zLBXB(cc4OV{}a$#S3Wo>xVP>OG&}ni^jF^_6?ikj74kbY f&f?o<{zE-ikbwnO`J2_55~2q -

-

💰 کیف پول

-
- - ریال - - - مسدود - - فعال +
+

💰 وضعیت مالی

+ + +
+
مانده حساب (موجودی منهای بدهی‌ها)
+
+ ریال +
+ 0): ?> +
+ ⚠️ بدهی در انتظار پرداخت: ریال +
+ + +
+
+
موجودی کیف پول
+
+ ریال +
+
+
+
وضعیت
+
+ + 🔴 مسدود + + 🟢 فعال + +
+
+
@@ -256,7 +279,11 @@ function ifnex_user_profile_shortcode($atts) {
کل سفارشات
-
+
+
در انتظار تأیید
+
+
+
در انتظار پرداخت
@@ -405,7 +432,7 @@ function ifnex_orders_list_shortcode($atts) { - + 💳 پرداخت @@ -695,11 +722,18 @@ function ifnex_order_payment_shortcode($atts) { $data = $order['data']; - // بررسی وضعیت - if ($data['status']['value'] !== 'pending_payment') { - return '
-

✅ این سفارش قبلاً پرداخت شده است

-

وضعیت: ' . esc_html($data['status']['label']) . '

+ // بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند) + if ($data['status']['value'] !== 'approved') { + $message = 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.'; + if ($data['status']['value'] === 'pending_approval') { + $message = 'این سفارش در انتظار تأیید کارمند است. پس از تأیید، گزینه پرداخت فعال خواهد شد.'; + } elseif ($data['status']['is_paid']) { + $message = 'این سفارش قبلاً پرداخت شده است.'; + } + + return '
'; } @@ -938,7 +972,7 @@ function ifnex_order_detail_shortcode($atts) { ← بازگشت به سفارشات - + 💳 پرداخت diff --git a/04_Laravel/app/Enums/ShipmentStatus.php b/04_Laravel/app/Enums/ShipmentStatus.php index e4b9355..2149b32 100644 --- a/04_Laravel/app/Enums/ShipmentStatus.php +++ b/04_Laravel/app/Enums/ShipmentStatus.php @@ -4,6 +4,8 @@ namespace App\Enums; enum ShipmentStatus: string { + case PendingApproval = 'pending_approval'; + case Approved = 'approved'; case PendingPayment = 'pending_payment'; case Cancelled = 'cancelled'; case Processed = 'processed'; @@ -18,15 +20,17 @@ enum ShipmentStatus: string public function label(): string { return match ($this) { + self::PendingApproval => 'در انتظار تأیید', + self::Approved => 'تأیید شده - در انتظار پرداخت', self::PendingPayment => 'در انتظار پرداخت', self::Cancelled => 'لغو شده', - self::Processed => 'Processed', - self::PickedUp => 'Picked Up', - self::InTransit => 'In Transit', - self::OutForDelivery => 'Out For Delivery', - self::Failed => 'Failed', - self::Delivered => 'Delivered', - self::Returned => 'Returned', + self::Processed => 'در حال پردازش', + self::PickedUp => 'تحویل به حمل‌کننده', + self::InTransit => 'در حال حمل', + self::OutForDelivery => 'آماده تحویل', + self::Failed => 'تحویل ناموفق', + self::Delivered => 'تحویل شده', + self::Returned => 'برگشتی', self::Archived => 'آرشیو', }; } @@ -34,6 +38,8 @@ enum ShipmentStatus: string public function color(): string { return match ($this) { + self::PendingApproval => 'warning', + self::Approved => 'info', self::PendingPayment => 'warning', self::Cancelled => 'danger', self::Processed => 'gray', @@ -49,11 +55,37 @@ enum ShipmentStatus: string public function isPaid(): bool { - return $this !== self::PendingPayment; + return !in_array($this, [ + self::PendingApproval, + self::PendingPayment, + ]); } public function canBeCancelledByCustomer(): bool { - return $this === self::PendingPayment; + return in_array($this, [ + self::PendingApproval, + self::PendingPayment, + ]); } -} \ No newline at end of file + + public function isApproved(): bool + { + return $this === self::Approved; + } + + public function isPendingApproval(): bool + { + return $this === self::PendingApproval; + } + + public function canBeApprovedByStaff(): bool + { + return $this === self::PendingApproval; + } + + public function canBePaid(): bool + { + return $this === self::Approved; + } +} diff --git a/04_Laravel/app/Filament/Pages/BulkTrackingImport.php b/04_Laravel/app/Filament/Pages/BulkTrackingImport.php new file mode 100644 index 0000000..d0ae96d --- /dev/null +++ b/04_Laravel/app/Filament/Pages/BulkTrackingImport.php @@ -0,0 +1,198 @@ +form->fill([ + 'csv_file' => null, + 'status' => 'in_transit', + 'description' => '', + ]); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Section::make('فایل CSV') + ->schema([ + Forms\Components\FileUpload::make('csv_file') + ->label('فایل CSV') + ->acceptedFileTypes(['text/csv', 'text/plain']) + ->maxSize(10240) // 10MB + ->required() + ->helperText('فرمت: awb_no,status,description (هر سطر یک سفارش)'), + ]), + + Forms\Components\Section::make('تنظیمات') + ->schema([ + Forms\Components\Select::make('status') + ->label('وضعیت جدید') + ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()) + ->default('in_transit') + ->required(), + Forms\Components\TextInput::make('description') + ->label('توضیحات پیش‌فرض') + ->placeholder('مثال: به‌روزرسانی گروهی وضعیت ترکینگ') + ->default('ایمپورت گروهی وضعیت'), + ]), + + Forms\Components\Section::make('پیش‌نمایش') + ->schema([ + Forms\Components\Placeholder::make('preview') + ->label('پیش‌نمایش فایل') + ->content(fn () => $this->getPreviewContent()), + ]) + ->collapsible(), + ]) + ->statePath('data'); + } + + private function getPreviewContent(): string + { + $file = $this->data['csv_file'] ?? null; + + if (!$file) { + return 'فایلی انتخاب نشده است. لطفاً فایل CSV را آپلود کنید.'; + } + + try { + $path = is_array($file) ? ($file['path'] ?? reset($file)) : $file; + $content = file_get_contents($path); + $lines = explode("\n", trim($content)); + + $preview = "
"; + $preview .= "خط اول (هدر): " . e($lines[0] ?? '') . "
"; + $preview .= "تعداد خطوط: " . count($lines) . "
"; + + if (isset($lines[1])) { + $preview .= "
نمونه (خط دوم): " . e($lines[1]); + } + + $preview .= "
"; + return $preview; + } catch (\Exception $e) { + return 'خطا در خواندن فایل: ' . $e->getMessage(); + } + } + + public function submit(): void + { + $data = $this->form->getState(); + + $file = $data['csv_file']; + $path = is_array($file) ? ($file['path'] ?? reset($file)) : $file; + $content = file_get_contents($path); + $lines = explode("\n", trim($content)); + + // حذف خط اول (هدر) + $dataLines = array_slice($lines, 1); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + DB::beginTransaction(); + + try { + foreach ($dataLines as $lineNumber => $line) { + $line = trim($line); + if (empty($line)) continue; + + $parts = str_getcsv($line); + + if (count($parts) < 1) { + $errors[] = "خط " . ($lineNumber + 2) . ": فرمت نادرست"; + $errorCount++; + continue; + } + + $awbNo = $parts[0]; + $status = $parts[1] ?? $data['status']; + $description = $parts[2] ?? $data['description']; + + // بررسی وجود سفارش + $shipment = Shipment::where('awb_no', $awbNo)->first(); + + if (!$shipment) { + $errors[] = "خط " . ($lineNumber + 2) . ": سفارش {$awbNo} یافت نشد"; + $errorCount++; + continue; + } + + // به‌روزرسانی وضعیت + $shipment->update([ + 'status' => $status, + 'tracking_description' => $description, + ]); + + // ثبت تاریخچه + DB::table('tracking_histories')->insert([ + 'shipment_id' => $shipment->id, + 'status' => $status, + 'description' => $description, + 'location' => 'سیستم ایمپورت گروهی', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $successCount++; + } + + DB::commit(); + + Notification::make() + ->title('ایمپورت با موفقیت انجام شد') + ->body("{$successCount} سفارش با موفقیت به‌روزرسانی شد. {$errorCount} خطا رخ داد.") + ->success() + ->send(); + + if (!empty($errors)) { + Notification::make() + ->title('خطاها') + ->body(implode("\n", array_slice($errors, 0, 10))) + ->warning() + ->send(); + } + + $this->form->fill([ + 'csv_file' => null, + 'status' => 'in_transit', + 'description' => '', + ]); + + } catch (\Exception $e) { + DB::rollBack(); + + Notification::make() + ->title('خطا در ایمپورت') + ->body($e->getMessage()) + ->danger() + ->send(); + } + } +} diff --git a/04_Laravel/app/Filament/Resources/AuditLogResource.php b/04_Laravel/app/Filament/Resources/AuditLogResource.php new file mode 100644 index 0000000..4301966 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/AuditLogResource.php @@ -0,0 +1,174 @@ +schema([ + Forms\Components\Section::make('اطلاعات لاگ') + ->schema([ + Forms\Components\Placeholder::make('event') + ->label('نوع رویداد') + ->content(fn ($record) => match($record->event) { + 'created' => '🟢 ایجاد', + 'updated' => '🟡 به‌روزرسانی', + 'deleted' => '🔴 حذف', + default => $record->event, + }), + Forms\Components\Placeholder::make('user') + ->label('کاربر') + ->content(fn ($record) => $record->user?->name ?? 'سیستم'), + Forms\Components\Placeholder::make('auditable') + ->label('رکورد') + ->content(fn ($record) => $record->auditable_type . ' #' . $record->auditable_id), + Forms\Components\Placeholder::make('created_at') + ->label('تاریخ و زمان') + ->content(fn ($record) => $record->created_at->format('Y-m-d H:i:s')), + ])->columns(2), + + Forms\Components\Section::make('تغییرات') + ->schema([ + Forms\Components\Placeholder::make('old_values') + ->label('مقادیر قبلی') + ->content(fn ($record) => $record->old_values ? json_encode($record->old_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-'), + Forms\Components\Placeholder::make('new_values') + ->label('مقادیر جدید') + ->content(fn ($record) => $record->new_values ? json_encode($record->new_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-'), + ])->columns(2), + + Forms\Components\Section::make('اطلاعات فنی') + ->schema([ + Forms\Components\Placeholder::make('description') + ->label('توضیحات') + ->content(fn ($record) => $record->description ?? '-'), + Forms\Components\Placeholder::make('ip_address') + ->label('آدرس IP') + ->content(fn ($record) => $record->ip_address ?? '-'), + Forms\Components\Placeholder::make('user_agent') + ->label('مرورگر') + ->content(fn ($record) => $record->user_agent ?? '-'), + ])->columns(2), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('شناسه') + ->sortable(), + Tables\Columns\TextColumn::make('user.name') + ->label('کاربر') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('event') + ->label('رویداد') + ->formatStateUsing(fn ($state) => match($state) { + 'created' => '🟢 ایجاد', + 'updated' => '🟡 به‌روزرسانی', + 'deleted' => '🔴 حذف', + default => $state, + }) + ->sortable(), + Tables\Columns\TextColumn::make('auditable_type') + ->label('نوع مدل') + ->formatStateUsing(fn ($state) => class_basename($state)) + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('auditable_id') + ->label('شناسه رکورد') + ->sortable(), + Tables\Columns\TextColumn::make('description') + ->label('توضیحات') + ->limit(50) + ->wrap(), + Tables\Columns\TextColumn::make('created_at') + ->label('تاریخ') + ->dateTime() + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('event') + ->label('نوع رویداد') + ->options([ + 'created' => 'ایجاد', + 'updated' => 'به‌روزرسانی', + 'deleted' => 'حذف', + ]), + Tables\Filters\SelectFilter::make('user_id') + ->label('کاربر') + ->relationship('user', 'name') + ->searchable(), + Tables\Filters\SelectFilter::make('auditable_type') + ->label('نوع مدل') + ->options([ + 'App\Models\Shipment' => 'سفارش', + 'App\Models\User' => 'کاربر', + 'App\Models\Wallet' => 'کیف پول', + 'App\Models\WalletTransaction' => 'تراکنش کیف پول', + 'App\Models\ShipmentChecklist' => 'چک‌لیست', + ]), + Tables\Filters\Filter::make('created_at') + ->label('تاریخ ایجاد') + ->form([ + Forms\Components\DatePicker::make('created_from') + ->label('از تاریخ'), + Forms\Components\DatePicker::make('created_until') + ->label('تا تاریخ'), + ]) + ->query(function ($query, array $data): Builder { + return $query + ->when($data['created_from'], fn ($query, $date) => $query->where('created_at', '>=', $date)) + ->when($data['created_until'], fn ($query, $date) => $query->where('created_at', '<=', $date)); + }), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListAuditLogs::route('/'), + 'edit' => Pages\EditAuditLog::route('/{record}/edit'), + ]; + } + + public static function getNavigationBadge(): ?string + { + return static::getModel()::where('created_at', '>=', now()->subDays(7))->count(); + } +} diff --git a/04_Laravel/app/Filament/Resources/AuditLogResource/Pages/EditAuditLog.php b/04_Laravel/app/Filament/Resources/AuditLogResource/Pages/EditAuditLog.php new file mode 100644 index 0000000..803344f --- /dev/null +++ b/04_Laravel/app/Filament/Resources/AuditLogResource/Pages/EditAuditLog.php @@ -0,0 +1,11 @@ +schema([ + Forms\Components\Section::make('اطلاعات تعهدنامه') + ->schema([ + Forms\Components\TextInput::make('title') + ->required() + ->maxLength(255) + ->label('عنوان'), + Forms\Components\Textarea::make('description') + ->nullable() + ->rows(3) + ->label('توضیحات'), + Forms\Components\Select::make('direction') + ->options([ + 'export' => 'صادرات', + 'import' => 'واردات', + 'both' => 'هر دو', + ]) + ->default('both') + ->required() + ->label('جهت ارسال'), + Forms\Components\Toggle::make('is_active') + ->default(true) + ->label('فعال'), + Forms\Components\TextInput::make('sort_order') + ->numeric() + ->default(0) + ->label('ترتیب نمایش'), + ])->columns(2), + + Forms\Components\Section::make('فایل') + ->schema([ + FileUpload::make('file_path') + ->label('فایل تعهدنامه') + ->disk('public') + ->directory('commitment-forms') + ->acceptedFileTypes(['application/pdf', 'image/png', 'image/jpeg']) + ->maxSize(10240) // 10MB + ->required() + ->columnSpanFull() + ->extraAttributes(['class' => 'w-full']) + ->getUploadedFileNameForStorageUsing(function ($file): string { + $extension = $file->getClientOriginalExtension(); + $filename = time() . '_' . uniqid() . '.' . $extension; + return $filename; + }), + Forms\Components\Placeholder::make('file_info') + ->label('اطلاعات فایل') + ->content(function ($record) { + if (!$record) return 'فایلی آپلود نشده'; + + $type = strtoupper(pathinfo($record->file_path, PATHINFO_EXTENSION)); + $size = $record->formatted_size; + + return "فرمت: {$type} | حجم: {$size}"; + }), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('title') + ->searchable() + ->label('عنوان'), + Tables\Columns\TextColumn::make('direction') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'export' => 'success', + 'import' => 'info', + 'both' => 'warning', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'export' => 'صادرات', + 'import' => 'واردات', + 'both' => 'هر دو', + }) + ->label('جهت'), + Tables\Columns\IconColumn::make('is_active') + ->boolean() + ->label('فعال'), + Tables\Columns\TextColumn::make('sort_order') + ->sortable() + ->label('ترتیب'), + Tables\Columns\TextColumn::make('uploader.name') + ->label('آپلود کننده'), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->label('تاریخ ایجاد'), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('direction') + ->options([ + 'export' => 'صادرات', + 'import' => 'واردات', + 'both' => 'هر دو', + ]), + Tables\Filters\TernaryFilter::make('is_active') + ->label('فعال'), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + Tables\Actions\Action::make('download') + ->label('دانلود') + ->icon('heroicon-o-arrow-down-tray') + ->color('info') + ->url(fn ($record) => $record->file_url) + ->openUrlInNewTab(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListCommitmentForms::route('/'), + 'create' => Pages\CreateCommitmentForm::route('/create'), + 'edit' => Pages\EditCommitmentForm::route('/{record}/edit'), + ]; + } +} diff --git a/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/CreateCommitmentForm.php b/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/CreateCommitmentForm.php new file mode 100644 index 0000000..1891762 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/CreateCommitmentForm.php @@ -0,0 +1,17 @@ +id(); + return $data; + } +} diff --git a/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/EditCommitmentForm.php b/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/EditCommitmentForm.php new file mode 100644 index 0000000..e6b7447 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/CommitmentFormResource/Pages/EditCommitmentForm.php @@ -0,0 +1,19 @@ +schema([ + Forms\Components\Section::make('اطلاعات مشتری') + ->schema([ + Forms\Components\TextInput::make('name') + ->label('نام') + ->disabled(), + Forms\Components\TextInput::make('email') + ->label('ایمیل') + ->disabled(), + Forms\Components\TextInput::make('phone') + ->label('موبایل') + ->disabled(), + ])->columns(3), + + Forms\Components\Section::make('اطلاعات اعتبار') + ->schema([ + Forms\Components\TextInput::make('credit_limit') + ->label('سقف اعتبار (ریال)') + ->numeric() + ->default(0) + ->minValue(0) + ->required(), + Forms\Components\TextInput::make('credit_used') + ->label('مبلغ استفاده شده (ریال)') + ->numeric() + ->default(0) + ->disabled(), + Forms\Components\TextInput::make('available_credit') + ->label('اعتبار باقیمانده (ریال)') + ->numeric() + ->disabled() + ->dehydrated(false), + ])->columns(3), + + Forms\Components\Section::make('تاریخچه تراکنش‌ها') + ->schema([ + Forms\Components\Placeholder::make('transactions_history') + ->label('تراکنش‌های اخیر') + ->content(function ($record) { + if (!$record) return '-'; + + $transactions = DB::table('wallet_transactions') + ->where('user_id', $record->id) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + if ($transactions->isEmpty()) return 'تراکنشی ثبت نشده'; + + $html = '
'; + foreach ($transactions as $tx) { + $type = match($tx->type) { + 'credit_add' => '➕ افزایش اعتبار', + 'credit_use' => '➖ استفاده از اعتبار', + default => $tx->type, + }; + $html .= '
' . $type . ': ' . number_format($tx->amount) . ' ریال - ' . $tx->description . '
'; + } + $html .= '
'; + return $html; + }), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('شناسه') + ->sortable(), + Tables\Columns\TextColumn::make('name') + ->label('نام') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('email') + ->label('ایمیل') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('phone') + ->label('موبایل') + ->searchable(), + Tables\Columns\TextColumn::make('credit_limit') + ->label('سقف اعتبار') + ->numeric() + ->formatStateUsing(fn ($state) => number_format($state) . ' ریال') + ->sortable(), + Tables\Columns\TextColumn::make('credit_used') + ->label('استفاده شده') + ->numeric() + ->formatStateUsing(fn ($state) => number_format($state) . ' ریال') + ->sortable(), + Tables\Columns\TextColumn::make('available_credit') + ->label('باقیمانده') + ->numeric() + ->formatStateUsing(fn ($state) => number_format($state) . ' ریال') + ->color(fn ($state) => $state > 0 ? 'success' : 'danger') + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('has_credit') + ->label('وضعیت اعتبار') + ->options([ + 'has_limit' => 'دارای سقف اعتبار', + 'no_limit' => 'بدون سقف اعتبار', + 'has_used' => 'استفاده شده', + 'has_available' => 'باقیمانده', + ]) + ->query(function ($query, $filter) { + match ($filter->getState()) { + 'has_limit' => $query->where('credit_limit', '>', 0), + 'no_limit' => $query->where('credit_limit', 0), + 'has_used' => $query->where('credit_used', '>', 0), + 'has_available' => $query->whereRaw('credit_limit - credit_used > 0'), + }; + }), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\Action::make('add_credit') + ->label('افزایش اعتبار') + ->icon('heroicon-o-plus-circle') + ->color('success') + ->form([ + Forms\Components\TextInput::make('amount') + ->label('مبلغ (ریال)') + ->numeric() + ->required() + ->minValue(1), + Forms\Components\TextInput::make('description') + ->label('توضیحات') + ->required(), + ]) + ->action(function ($record, array $data): void { + DB::beginTransaction(); + try { + $record->credit_limit += $data['amount']; + $record->save(); + + DB::table('wallet_transactions')->insert([ + 'user_id' => $record->id, + 'type' => 'credit_add', + 'amount' => $data['amount'], + 'description' => $data['description'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + }), + Tables\Actions\Action::make('reduce_credit') + ->label('کاهش اعتبار') + ->icon('heroicon-o-minus-circle') + ->color('danger') + ->form([ + Forms\Components\TextInput::make('amount') + ->label('مبلغ (ریال)') + ->numeric() + ->required() + ->minValue(1), + Forms\Components\TextInput::make('description') + ->label('توضیحات') + ->required(), + ]) + ->action(function ($record, array $data): void { + DB::beginTransaction(); + try { + $record->credit_limit -= $data['amount']; + $record->save(); + + DB::table('wallet_transactions')->insert([ + 'user_id' => $record->id, + 'type' => 'credit_reduce', + 'amount' => $data['amount'], + 'description' => $data['description'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + throw $e; + } + }), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListCustomerCredits::route('/'), + 'edit' => Pages\EditCustomerCredit::route('/{record}/edit'), + ]; + } + + public static function getNavigationBadge(): ?string + { + return static::getModel()::where('credit_limit', '>', 0)->count(); + } +} diff --git a/04_Laravel/app/Filament/Resources/CustomerCreditResource/Pages/EditCustomerCredit.php b/04_Laravel/app/Filament/Resources/CustomerCreditResource/Pages/EditCustomerCredit.php new file mode 100644 index 0000000..92d9d8f --- /dev/null +++ b/04_Laravel/app/Filament/Resources/CustomerCreditResource/Pages/EditCustomerCredit.php @@ -0,0 +1,19 @@ +schema([ + Forms\Components\Section::make('اطلاعات سفارش') + ->schema([ + Forms\Components\Select::make('shipment_id') + ->label('سفارش') + ->relationship('shipment', 'awb_no') + ->searchable() + ->required() + ->getOptionLabelFromRecordUsing(fn ($record) => "{$record->awb_no} - {$record->status->label()}"), + Forms\Components\Select::make('user_id') + ->label('کارمند') + ->relationship('user', 'name') + ->searchable() + ->required(), + ])->columns(2), + + Forms\Components\Section::make('اطلاعات چک‌لیست') + ->schema([ + Forms\Components\Select::make('status') + ->label('وضعیت فعلی') + ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()) + ->required(), + Forms\Components\Toggle::make('is_completed') + ->label('تکمیل شده') + ->default(false), + Forms\Components\Textarea::make('notes') + ->label('یادداشت‌ها') + ->rows(3), + Forms\Components\DateTimePicker::make('completed_at') + ->label('زمان تکمیل') + ->disabled(), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('شناسه') + ->sortable(), + Tables\Columns\TextColumn::make('shipment.awb_no') + ->label('شماره سفارش') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('user.name') + ->label('کارمند') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('status') + ->label('وضعیت') + ->formatStateUsing(fn ($state) => match($state) { + 'pending_approval' => 'در انتظار تأیید', + 'approved' => 'تأیید شده', + 'pending_payment' => 'در انتظار پرداخت', + 'paid' => 'پرداخت شده', + 'processed' => 'در حال پردازش', + 'picked_up' => 'جمع‌آوری شده', + 'in_transit' => 'در حال ارسال', + 'out_for_delivery' => 'آماده تحویل', + 'delivered' => 'تحویل شده', + 'cancelled' => 'لغو شده', + default => $state, + }) + ->badge() + ->color(fn ($state) => match($state) { + 'pending_approval' => 'warning', + 'approved' => 'info', + 'pending_payment' => 'warning', + 'paid' => 'success', + 'processed' => 'info', + 'picked_up' => 'info', + 'in_transit' => 'primary', + 'out_for_delivery' => 'success', + 'delivered' => 'success', + 'cancelled' => 'danger', + default => 'gray', + }), + Tables\Columns\IconColumn::make('is_completed') + ->label('تکمیل') + ->boolean(), + Tables\Columns\TextColumn::make('notes') + ->label('یادداشت') + ->limit(50) + ->wrap(), + Tables\Columns\TextColumn::make('created_at') + ->label('تاریخ ایجاد') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('completed_at') + ->label('زمان تکمیل') + ->dateTime() + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('is_completed') + ->label('وضعیت تکمیل') + ->options([ + 'completed' => 'تکمیل شده', + 'pending' => 'در انتظار', + ]) + ->query(function ($query, $filter) { + match ($filter->getState()) { + 'completed' => $query->where('is_completed', true), + 'pending' => $query->where('is_completed', false), + }; + }), + Tables\Filters\SelectFilter::make('status') + ->label('وضعیت سفارش') + ->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()), + Tables\Filters\SelectFilter::make('user_id') + ->label('کارمند') + ->relationship('user', 'name') + ->searchable(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\Action::make('complete') + ->label('تکمیل') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->modalHeading('تأیید تکمیل') + ->modalDescription('آیا از تکمیل این چک‌لیست اطمینان دارید؟') + ->action(fn ($record) => $record->complete()) + ->visible(fn ($record) => !$record->is_completed), + Tables\Actions\Action::make('add_note') + ->label('افزودن یادداشت') + ->icon('heroicon-o-chat-bubble-bottom-center-text') + ->color('info') + ->form([ + Forms\Components\Textarea::make('note') + ->label('یادداشت جدید') + ->required(), + ]) + ->action(function ($record, array $data): void { + $existingNotes = $record->notes ? $record->notes . "\n\n" : ''; + $record->update([ + 'notes' => $existingNotes . "[" . now()->format('Y-m-d H:i') . "] " . $data['note'], + ]); + }), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShipmentChecklists::route('/'), + 'create' => Pages\CreateShipmentChecklist::route('/create'), + 'edit' => Pages\EditShipmentChecklist::route('/{record}/edit'), + ]; + } + + public static function getNavigationBadge(): ?string + { + return static::getModel()::where('is_completed', false)->count(); + } +} diff --git a/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/CreateShipmentChecklist.php b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/CreateShipmentChecklist.php new file mode 100644 index 0000000..4cca41f --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentChecklistResource/Pages/CreateShipmentChecklist.php @@ -0,0 +1,11 @@ +label('Cash on Delivery'), ])->columns(4), + Forms\Components\Section::make('Import Invoice Fields (Service Sales Invoice)') + ->schema([ + Forms\Components\TextInput::make('brand_fee') + ->numeric() + ->default(0) + ->prefix('ریال') + ->label('Brand Fee'), + Forms\Components\TextInput::make('report_fee') + ->numeric() + ->default(0) + ->prefix('ریال') + ->label('Report Fee'), + Forms\Components\TextInput::make('customs_clearance_cost') + ->numeric() + ->default(0) + ->prefix('ریال') + ->label('Customs Clearance Cost'), + Forms\Components\TextInput::make('order_registration_fee') + ->numeric() + ->default(0) + ->prefix('ریال') + ->label('Order Registration Fee'), + Forms\Components\TextInput::make('other_clearance_charges') + ->numeric() + ->default(0) + ->prefix('ریال') + ->label('Other Clearance Charges'), + Forms\Components\TextInput::make('exchange_rate') + ->numeric() + ->default(0) + ->label('Exchange Rate'), + Forms\Components\TextInput::make('goods_nature') + ->nullable() + ->label('Goods Nature'), + Forms\Components\Select::make('invoice_currency') + ->options([ + 'USD' => 'USD', + 'EUR' => 'EUR', + 'AED' => 'AED', + 'IRR' => 'IRR', + ]) + ->default('USD') + ->label('Invoice Currency'), + ])->columns(4), + Forms\Components\Section::make('Sender Info') ->schema([ Forms\Components\TextInput::make('sender_name') @@ -406,6 +451,32 @@ class ShipmentResource extends Resource ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), + Action::make('approve') + ->label('تأیید') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->modalHeading('تأیید سفارش') + ->modalDescription('آیا از تأیید این سفارش اطمینان دارید؟ پس از تأیید، مشتری می‌تواند پرداخت را انجام دهد.') + ->modalSubmitActionLabel('بله، تأیید کن') + ->visible(fn ($record) => $record->status === \App\Enums\ShipmentStatus::PendingApproval) + ->action(function ($record) { + $oldStatus = $record->status; + $record->update(['status' => \App\Enums\ShipmentStatus::Approved]); + + \App\Models\ShipmentStatusHistory::create([ + 'shipment_id' => $record->id, + 'from_status' => $oldStatus->value, + 'to_status' => \App\Enums\ShipmentStatus::Approved->value, + 'reason' => 'تأیید توسط مدیر از پنل Filament', + 'changed_by' => auth()->id(), + ]); + + \Filament\Notifications\Notification::make() + ->title('سفارش با موفقیت تأیید شد') + ->success() + ->send(); + }), ]) ->headerActions([ Action::make('exportCsv') diff --git a/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php new file mode 100644 index 0000000..aa33d99 --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php @@ -0,0 +1,66 @@ +where('is_active', true) + ->orderBy('sort_order') + ->orderBy('created_at', 'desc') + ->get() + ->map(fn ($form) => [ + 'id' => $form->id, + 'title' => $form->title, + 'description' => $form->description, + 'file_url' => $form->file_url, + 'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)), + 'direction' => $form->direction, + ]); + + return response()->json([ + 'success' => true, + 'data' => $forms, + ]); + } + + /** + * دریافت تعهدنامه‌ها بر اساس جهت ارسال + * GET /api/v1/commitment-forms/{direction} + */ + public function byDirection(string $direction): JsonResponse + { + $forms = CommitmentForm::query() + ->where('is_active', true) + ->where(function ($query) use ($direction) { + $query->where('direction', 'both') + ->orWhere('direction', $direction); + }) + ->orderBy('sort_order') + ->orderBy('created_at', 'desc') + ->get() + ->map(fn ($form) => [ + 'id' => $form->id, + 'title' => $form->title, + 'description' => $form->description, + 'file_url' => $form->file_url, + 'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)), + 'direction' => $form->direction, + ]); + + return response()->json([ + 'success' => true, + 'data' => $forms, + ]); + } +} diff --git a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php index f12b373..b3b67b2 100644 --- a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php +++ b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php @@ -212,7 +212,7 @@ class CustomerOrderController extends Controller 'awb_no' => $awbNo, 'direction' => $validated['direction'], 'type' => $validated['type'], - 'status' => ShipmentStatus::PendingPayment, + 'status' => ShipmentStatus::PendingApproval, // وزن 'weight' => $validated['weight'], @@ -307,7 +307,7 @@ class CustomerOrderController extends Controller return response()->json([ 'success' => true, - 'message' => 'سفارش شما با موفقیت ثبت شد. لطفاً برای تکمیل، پرداخت را انجام دهید.', + 'message' => 'سفارش شما با موفقیت ثبت شد. پس از تأیید کارمند، گزینه پرداخت برای شما فعال خواهد شد.', 'data' => $this->formatShipment($shipment->load(['fromCountry', 'toCountry', 'items'])), 'pricing' => $priceResult, ], 201); @@ -384,12 +384,25 @@ class CustomerOrderController extends Controller $user = Auth::user(); $user->load(['wallet']); + // محاسبه مجموع سفارشات در انتظار پرداخت (بدهی کاربر) + $pending_payments_total = Shipment::where('user_id', $user->id) + ->where('status', ShipmentStatus::Approved) + ->sum('total_fee'); + + // مانده حساب = موجودی کیف پول - بدهی‌ها + $wallet_balance = $user->wallet ? $user->wallet->balance : 0; + $account_balance = $wallet_balance - $pending_payments_total; + // آمار سفارشات $stats = [ 'total_orders' => Shipment::where('user_id', $user->id)->count(), - 'pending_orders' => Shipment::where('user_id', $user->id) - ->where('status', ShipmentStatus::PendingPayment) + 'pending_approval' => Shipment::where('user_id', $user->id) + ->where('status', ShipmentStatus::PendingApproval) ->count(), + 'pending_payment' => Shipment::where('user_id', $user->id) + ->where('status', ShipmentStatus::Approved) + ->count(), + 'pending_payment_total' => $pending_payments_total, 'active_orders' => Shipment::where('user_id', $user->id) ->whereIn('status', [ ShipmentStatus::Processed, @@ -402,7 +415,8 @@ class CustomerOrderController extends Controller ->where('status', ShipmentStatus::Delivered) ->count(), 'total_spent' => Shipment::where('user_id', $user->id) - ->where('status', '!=', ShipmentStatus::PendingPayment) + ->where('status', '!=', ShipmentStatus::PendingApproval) + ->where('status', '!=', ShipmentStatus::Approved) ->where('status', '!=', ShipmentStatus::Cancelled) ->sum('total_fee'), ]; @@ -420,6 +434,8 @@ class CustomerOrderController extends Controller 'balance' => $user->wallet->balance, 'is_frozen' => $user->wallet->is_frozen, ] : null, + 'account_balance' => $account_balance, + 'pending_payments_total' => $pending_payments_total, 'stats' => $stats, ]); } @@ -457,6 +473,8 @@ class CustomerOrderController extends Controller 'color' => $shipment->status?->color(), 'is_paid' => $shipment->status?->isPaid(), 'can_cancel' => $shipment->status?->canBeCancelledByCustomer(), + 'can_pay' => $shipment->status?->canBePaid(), + 'is_approved' => $shipment->status?->isApproved(), ], 'from_country' => $shipment->fromCountry ? [ 'id' => $shipment->fromCountry->id, @@ -565,11 +583,11 @@ class CustomerOrderController extends Controller ], 403); } - // بررسی وضعیت - if ($shipment->status !== ShipmentStatus::PendingPayment) { + // بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند) + if ($shipment->status !== ShipmentStatus::Approved) { return response()->json([ 'success' => false, - 'message' => 'این سفارش در وضعیت قابل پرداخت نیست.', + 'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.', ], 400); } @@ -611,11 +629,11 @@ class CustomerOrderController extends Controller ], 403); } - // بررسی وضعیت - if ($shipment->status !== ShipmentStatus::PendingPayment) { + // بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند) + if ($shipment->status !== ShipmentStatus::Approved) { return response()->json([ 'success' => false, - 'message' => 'این سفارش در وضعیت قابل پرداخت نیست.', + 'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.', ], 400); } diff --git a/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php b/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php new file mode 100644 index 0000000..ada7202 --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php @@ -0,0 +1,135 @@ +validate([ + 'q' => ['required', 'string', 'min:2'], + ]); + + $query = User::query() + ->where(function ($q) use ($validated) { + $q->where('name', 'LIKE', "%{$validated['q']}%") + ->orWhere('email', 'LIKE', "%{$validated['q']}%") + ->orWhere('phone', 'LIKE', "%{$validated['q']}%"); + }) + ->with('wallet'); + + $customers = $query->take(20)->get()->map(function ($customer) { + return [ + 'id' => $customer->id, + 'name' => $customer->name, + 'email' => $customer->email, + 'phone' => $customer->phone, + 'wallet_balance' => $customer->wallet ? $customer->wallet->balance : 0, + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $customers, + ]); + } + + /** + * دریافت وضعیت مالی کامل مشتری + * GET /api/v1/staff/customers/{customer}/financial-status + */ + public function financialStatus(User $customer): JsonResponse + { + // دریافت سفارشات در انتظار پرداخت (تأیید شده) + $pending_orders = Shipment::where('user_id', $customer->id) + ->where('status', ShipmentStatus::Approved) + ->with(['fromCountry', 'toCountry']) + ->orderBy('created_at', 'desc') + ->get(); + + // محاسبه بدهی به هر ارز + $debts_by_currency = $pending_orders->groupBy(function ($order) { + return $order->fromCountry?->iso_code ?? 'unknown'; + })->map(function ($orders, $currency) { + return [ + 'currency' => $currency, + 'total' => $orders->sum('total_fee'), + 'count' => $orders->count(), + ]; + }); + + // دریافت تراکنش‌های اخیر + $recent_transactions = WalletTransaction::where('user_id', $customer->id) + ->orderBy('created_at', 'desc') + ->take(10) + ->get() + ->map(fn ($tx) => [ + 'id' => $tx->id, + 'type' => $tx->type, + 'amount' => $tx->amount, + 'description' => $tx->description, + 'created_at' => $tx->created_at->format('Y-m-d H:i'), + ]); + + // دریافت سفارشات اخیر + $recent_orders = Shipment::where('user_id', $customer->id) + ->with(['fromCountry', 'toCountry']) + ->orderBy('created_at', 'desc') + ->take(10) + ->get() + ->map(fn ($order) => [ + 'id' => $order->id, + 'awb_no' => $order->awb_no, + 'status' => [ + 'value' => $order->status?->value, + 'label' => $order->status?->label(), + ], + 'direction' => $order->direction?->value, + 'total_fee' => $order->total_fee, + 'from_country' => $order->fromCountry?->name, + 'to_country' => $order->toCountry?->name, + 'created_at' => $order->created_at->format('Y-m-d H:i'), + ]); + + // محاسبه کل بدهی + $total_debt = $pending_orders->sum('total_fee'); + + // موجودی کیف پول + $wallet_balance = $customer->wallet ? $customer->wallet->balance : 0; + + // مانده حساب + $account_balance = $wallet_balance - $total_debt; + + return response()->json([ + 'success' => true, + 'customer' => [ + 'id' => $customer->id, + 'name' => $customer->name, + 'email' => $customer->email, + 'phone' => $customer->phone, + ], + 'financial' => [ + 'wallet_balance' => $wallet_balance, + 'total_debt' => $total_debt, + 'account_balance' => $account_balance, + 'debts_by_currency' => array_values($debts_by_currency->toArray()), + 'pending_orders_count' => $pending_orders->count(), + ], + 'recent_transactions' => $recent_transactions, + 'recent_orders' => $recent_orders, + ]); + } +} diff --git a/04_Laravel/app/Http/Controllers/Api/MobileVerificationController.php b/04_Laravel/app/Http/Controllers/Api/MobileVerificationController.php new file mode 100644 index 0000000..90e04cf --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Api/MobileVerificationController.php @@ -0,0 +1,104 @@ +smsService = $smsService; + } + + /** + * ارسال کد تأیید به شماره موبایل + * POST /api/v1/verify/send-code + */ + public function sendCode(Request $request): JsonResponse + { + $validated = $request->validate([ + 'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'], + ]); + + $result = $this->smsService->sendVerificationCode($validated['phone']); + + if ($result['success']) { + return response()->json([ + 'success' => true, + 'message' => 'کد تأیید با موفقیت ارسال شد', + ]); + } + + return response()->json([ + 'success' => false, + 'message' => $result['message'], + ], 400); + } + + /** + * بررسی کد تأیید + * POST /api/v1/verify/check-code + */ + public function checkCode(Request $request): JsonResponse + { + $validated = $request->validate([ + 'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'], + 'code' => ['required', 'string', 'size:5'], + ]); + + $isValid = $this->smsService->verifyCode($validated['phone'], $validated['code']); + + if ($isValid) { + // به‌روزرسانی شماره موبایل کاربر (اختیاری) + $user = Auth::user(); + if ($user && $user->phone !== $validated['phone']) { + $user->update(['phone' => $validated['phone']]); + } + + return response()->json([ + 'success' => true, + 'message' => 'کد تأیید صحیح است', + ]); + } + + return response()->json([ + 'success' => false, + 'message' => 'کد تأیید نادرست است یا منقضی شده است', + ], 400); + } + + /** + * ارسال پیامک به شماره موبایل (برای استفاده داخلی) + * POST /api/v1/verify/send-sms + */ + public function sendSms(Request $request): JsonResponse + { + $validated = $request->validate([ + 'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'], + 'message' => ['required', 'string', 'max:500'], + ]); + + $result = $this->smsService->send($validated['phone'], $validated['message']); + + if ($result['success']) { + return response()->json([ + 'success' => true, + 'message' => 'پیامک با موفقیت ارسال شد', + 'cost' => $result['cost'] ?? 0, + ]); + } + + return response()->json([ + 'success' => false, + 'message' => $result['message'], + ], 400); + } +} diff --git a/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php b/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php new file mode 100644 index 0000000..ade6976 --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php @@ -0,0 +1,201 @@ +validate([ + 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], + ]); + + $shipments = Shipment::query() + ->where('status', ShipmentStatus::PendingApproval) + ->with(['fromCountry', 'toCountry', 'user']) + ->orderBy('created_at', 'asc') + ->paginate($validated['per_page'] ?? 15); + + return response()->json([ + 'success' => true, + 'data' => $shipments->map(function ($shipment) { + return $this->formatShipment($shipment); + }), + 'pagination' => [ + 'current_page' => $shipments->currentPage(), + 'last_page' => $shipments->lastPage(), + 'per_page' => $shipments->perPage(), + 'total' => $shipments->total(), + ], + ]); + } + + /** + * تأیید سفارش توسط کارمند + * POST /api/v1/staff/orders/{shipment}/approve + */ + public function approve(Shipment $shipment, Request $request): JsonResponse + { + $user = Auth::user(); + + // بررسی وضعیت + if ($shipment->status !== ShipmentStatus::PendingApproval) { + return response()->json([ + 'success' => false, + 'message' => 'این سفارش در وضعیت قابل تأیید نیست.', + ], 400); + } + + $validated = $request->validate([ + 'notes' => ['nullable', 'string', 'max:1000'], + ]); + + try { + DB::transaction(function () use ($shipment, $user, $validated) { + // تغییر وضعیت به Approved + $oldStatus = $shipment->status; + $shipment->update([ + 'status' => ShipmentStatus::Approved, + ]); + + // ثبت در تاریخچه تغییرات + ShipmentStatusHistory::create([ + 'shipment_id' => $shipment->id, + 'old_status' => $oldStatus->value, + 'new_status' => ShipmentStatus::Approved->value, + 'changed_by' => $user->id, + 'notes' => $validated['notes'] ?? 'تأیید توسط کارمند', + ]); + }); + + return response()->json([ + 'success' => true, + 'message' => 'سفارش با موفقیت تأیید شد. مشتری می‌تواند پرداخت را انجام دهد.', + 'data' => $this->formatShipment($shipment->fresh()->load(['fromCountry', 'toCountry'])), + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'خطا در تأیید سفارش: ' . $e->getMessage(), + ], 500); + } + } + + /** + * رد سفارش توسط کارمند + * POST /api/v1/staff/orders/{shipment}/reject + */ + public function reject(Shipment $shipment, Request $request): JsonResponse + { + $user = Auth::user(); + + // بررسی وضعیت + if ($shipment->status !== ShipmentStatus::PendingApproval) { + return response()->json([ + 'success' => false, + 'message' => 'این سفارش در وضعیت قابل رد نیست.', + ], 400); + } + + $validated = $request->validate([ + 'reason' => ['required', 'string', 'max:1000'], + ]); + + try { + DB::transaction(function () use ($shipment, $user, $validated) { + // تغییر وضعیت به Cancelled + $oldStatus = $shipment->status; + $shipment->update([ + 'status' => ShipmentStatus::Cancelled, + ]); + + // ثبت در تاریخچه تغییرات + ShipmentStatusHistory::create([ + 'shipment_id' => $shipment->id, + 'old_status' => $oldStatus->value, + 'new_status' => ShipmentStatus::Cancelled->value, + 'changed_by' => $user->id, + 'notes' => 'رد شده: ' . $validated['reason'], + ]); + }); + + return response()->json([ + 'success' => true, + 'message' => 'سفارش رد شد.', + 'data' => $this->formatShipment($shipment->fresh()->load(['fromCountry', 'toCountry'])), + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'خطا در رد سفارش: ' . $e->getMessage(), + ], 500); + } + } + + /** + * فرمت کردن Shipment برای خروجی API + */ + private function formatShipment(Shipment $shipment): array + { + return [ + 'id' => $shipment->id, + 'awb_no' => $shipment->awb_no, + 'direction' => $shipment->direction?->value, + 'type' => $shipment->type?->value, + 'status' => [ + 'value' => $shipment->status?->value, + 'label' => $shipment->status?->label(), + 'color' => $shipment->status?->color(), + ], + 'from_country' => $shipment->fromCountry ? [ + 'id' => $shipment->fromCountry->id, + 'name' => $shipment->fromCountry->name, + 'iso_code' => $shipment->fromCountry->iso_code, + ] : null, + 'to_country' => $shipment->toCountry ? [ + 'id' => $shipment->toCountry->id, + 'name' => $shipment->toCountry->name, + 'iso_code' => $shipment->toCountry->iso_code, + ] : null, + 'user' => $shipment->user ? [ + 'id' => $shipment->user->id, + 'name' => $shipment->user->name, + 'email' => $shipment->user->email, + ] : null, + 'weight' => $shipment->weight, + 'chargeable_weight' => $shipment->chargeable_weight, + 'total_fee' => $shipment->total_fee, + 'created_at' => $shipment->created_at->toIso8601String(), + 'created_at_jalali' => $this->toJalali($shipment->created_at), + ]; + } + + /** + * تبدیل تاریخ به شمسی + */ + private function toJalali($date): string + { + if (!$date) return '—'; + + try { + return \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d H:i'); + } catch (\Exception $e) { + return $date->format('Y-m-d H:i'); + } + } +} diff --git a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php index dcf9d6f..621d23f 100644 --- a/04_Laravel/app/Http/Controllers/ShipmentPdfController.php +++ b/04_Laravel/app/Http/Controllers/ShipmentPdfController.php @@ -128,4 +128,56 @@ class ShipmentPdfController extends Controller return response('PDF generation failed: ' . $e->getMessage(), 500); } } + + /** + * تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice + */ + public function importInvoice(Shipment $shipment, Request $request) + { + try { + // دریافت پارامترها از درخواست + $options = $request->only([ + 'invoice_no', + 'invoice_date', + 'customs_office', + 'exchange_rate', + 'international_freight', + 'pick_up_fee', + 'brand_fee', + 'report_fee', + 'other_charges', + 'customs_clearance', + 'domestic_transport', + 'warehousing_fee', + 'order_registration_fee', + 'other_clearance_charges', + 'terms_and_conditions', + ]); + + // تبدیل exchange_rate به عدد + if (isset($options['exchange_rate'])) { + $options['exchange_rate'] = (float) $options['exchange_rate']; + } + + $content = $this->pdf->importInvoice($shipment, $options); + + Log::info('Import Invoice PDF generated successfully', [ + 'awb' => $shipment->awb_no, + 'options' => $options, + ]); + + return response($content, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="SERVICE-INVOICE-' . $shipment->awb_no . '.pdf"', + ]); + } catch (\Throwable $e) { + Log::error('Import Invoice PDF generation failed', [ + 'shipment_id' => $shipment->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return response('PDF generation failed: ' . $e->getMessage(), 500); + } + } } diff --git a/04_Laravel/app/Models/AuditLog.php b/04_Laravel/app/Models/AuditLog.php new file mode 100644 index 0000000..8927b8b --- /dev/null +++ b/04_Laravel/app/Models/AuditLog.php @@ -0,0 +1,65 @@ + 'array', + 'new_values' => 'array', + ]; + + // ─── Relationships ─────────────────────────────── + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function auditable() + { + return $this->morphTo(); + } + + // ─── Helper Methods ────────────────────────────── + + public static function log( + string $event, + Model $model, + array $oldValues = [], + array $newValues = [], + ?string $description = null + ): self { + return static::create([ + 'user_id' => auth()->id(), + 'event' => $event, + 'auditable_type' => get_class($model), + 'auditable_id' => $model->getKey(), + 'old_values' => $oldValues, + 'new_values' => $newValues, + 'description' => $description, + 'ip_address' => request()->ip(), + 'user_agent' => request()->userAgent(), + ]); + } +} diff --git a/04_Laravel/app/Models/CommitmentForm.php b/04_Laravel/app/Models/CommitmentForm.php new file mode 100644 index 0000000..800223f --- /dev/null +++ b/04_Laravel/app/Models/CommitmentForm.php @@ -0,0 +1,61 @@ + 'boolean', + 'sort_order' => 'integer', + 'file_size' => 'integer', + ]; + + /** + * کاربری که فایل را آپلود کرده + */ + public function uploader(): BelongsTo + { + return $this->belongsTo(User::class, 'uploaded_by'); + } + + /** + * آدرس کامل فایل + */ + public function getFileUrlAttribute(): string + { + return asset('storage/' . $this->file_path); + } + + /** + * فرمت حجم فایل + */ + public function getFormattedSizeAttribute(): string + { + $bytes = $this->file_size; + $units = ['B', 'KB', 'MB', 'GB']; + + for ($i = 0; $bytes >= 1024 && $i < count($units) - 1; $i++) { + $bytes /= 1024; + } + + return round($bytes, 2) . ' ' . $units[$i]; + } +} diff --git a/04_Laravel/app/Models/Shipment.php b/04_Laravel/app/Models/Shipment.php index 204e868..4055bf8 100644 --- a/04_Laravel/app/Models/Shipment.php +++ b/04_Laravel/app/Models/Shipment.php @@ -34,6 +34,16 @@ class Shipment extends Model 'net_dirham', 'net_rial', 'invoice_total_usd', + 'cod_amount', + // Import Invoice Fields + 'brand_fee', + 'report_fee', + 'customs_clearance_cost', + 'order_registration_fee', + 'other_clearance_charges', + 'exchange_rate', + 'goods_nature', + 'invoice_currency', // Sender 'sender_name', 'sender_company', @@ -55,6 +65,8 @@ class Shipment extends Model // Customs 'reason_for_export', 'content_description', + // User + 'user_id', ]; protected $casts = [ diff --git a/04_Laravel/app/Models/ShipmentChecklist.php b/04_Laravel/app/Models/ShipmentChecklist.php new file mode 100644 index 0000000..55e7b0f --- /dev/null +++ b/04_Laravel/app/Models/ShipmentChecklist.php @@ -0,0 +1,48 @@ + 'boolean', + 'completed_at' => 'datetime', + ]; + + // ─── Relationships ─────────────────────────────── + + public function shipment() + { + return $this->belongsTo(Shipment::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + // ─── Helper Methods ────────────────────────────── + + public function complete(): void + { + $this->update([ + 'is_completed' => true, + 'completed_at' => now(), + ]); + } +} diff --git a/04_Laravel/app/Models/User.php b/04_Laravel/app/Models/User.php index 8e46e64..93ca6fd 100644 --- a/04_Laravel/app/Models/User.php +++ b/04_Laravel/app/Models/User.php @@ -18,6 +18,8 @@ class User extends Authenticatable 'password', 'phone', 'role', // فیلد قدیمی - بعداً حذف می‌کنیم + 'credit_limit', // سقف اعتبار + 'credit_used', // مبلغ استفاده شده از اعتبار ]; protected $hidden = [ @@ -30,6 +32,8 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'credit_limit' => 'decimal:2', + 'credit_used' => 'decimal:2', ]; } @@ -62,6 +66,18 @@ class User extends Authenticatable return $this->hasRole('customer'); } + // ─── Credit Methods ────────────────────────────── + + public function getAvailableCreditAttribute(): float + { + return $this->credit_limit - $this->credit_used; + } + + public function hasAvailableCredit(float $amount): bool + { + return $this->available_credit >= $amount; + } + // ─── Filament Panel Access ─────────────────────── public function canAccessPanel(\Filament\Panel $panel): bool diff --git a/04_Laravel/app/Services/KavenegarSmsService.php b/04_Laravel/app/Services/KavenegarSmsService.php new file mode 100644 index 0000000..8f9c885 --- /dev/null +++ b/04_Laravel/app/Services/KavenegarSmsService.php @@ -0,0 +1,169 @@ +apiKey = config('services.kavenegar.api_key', ''); + $this->sender = config('services.kavenegar.sender', ''); + } + + /** + * ارسال کد تأیید به شماره موبایل + */ + public function sendVerificationCode(string $phone): array + { + $code = Str::random(5, '0123456789'); + $cacheKey = "sms_verify_{$phone}"; + + // ذخیره کد در cache به مدت ۱۰ دقیقه + Cache::put($cacheKey, $code, now()->addMinutes(10)); + + $message = "کد تأیید IFNEX: {$code}"; + + return $this->send($phone, $message); + } + + /** + * بررسی کد تأیید + */ + public function verifyCode(string $phone, string $code): bool + { + $cacheKey = "sms_verify_{$phone}"; + $storedCode = Cache::get($cacheKey); + + if ($storedCode && $storedCode === $code) { + Cache::forget($cacheKey); + return true; + } + + return false; + } + + /** + * ارسال پیامک + */ + public function send(string $receptor, string $message): array + { + if (empty($this->apiKey)) { + return [ + 'success' => false, + 'message' => 'API Key Kavenegar تنظیم نشده است', + ]; + } + + try { + $response = Http::timeout(10) + ->post("https://api.kavenegar.com/v1/{$this->apiKey}/sms/send.json", [ + 'receptor' => $receptor, + 'sender' => $this->sender, + 'message' => $message, + ]); + + $data = $response->json(); + + if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) { + return [ + 'success' => true, + 'message' => 'پیامک با موفقیت ارسال شد', + 'cost' => $data['entries'][0]['cost'] ?? 0, + ]; + } + + return [ + 'success' => false, + 'message' => $data['return']['message'] ?? 'خطا در ارسال پیامک', + ]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(), + ]; + } + } + + /** + * ارسال پیامک گروهی + */ + public function sendBulk(array $receptors, string $message): array + { + if (empty($this->apiKey)) { + return [ + 'success' => false, + 'message' => 'API Key Kavenegar تنظیم نشده است', + ]; + } + + try { + $response = Http::timeout(30) + ->post("https://api.kavenegar.com/v1/{$this->apiKey}/sms/send.json", [ + 'receptor' => $receptors, + 'sender' => $this->sender, + 'message' => $message, + ]); + + $data = $response->json(); + + if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) { + return [ + 'success' => true, + 'message' => 'پیامک با موفقیت ارسال شد', + 'cost' => collect($data['entries'])->sum('cost'), + ]; + } + + return [ + 'success' => false, + 'message' => $data['return']['message'] ?? 'خطا در ارسال پیامک', + ]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(), + ]; + } + } + + /** + * بررسی وضعیت پیامک ارسال شده + */ + public function checkStatus(string $messageId): array + { + try { + $response = Http::timeout(10) + ->get("https://api.kavenegar.com/v1/{$this->apiKey}/select.json", [ + 'messageid' => $messageId, + ]); + + $data = $response->json(); + + if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) { + $entry = $data['entries'][0] ?? null; + return [ + 'success' => true, + 'status' => $entry['status'] ?? 'unknown', + 'status_text' => $entry['statustext'] ?? 'نامشخص', + ]; + } + + return [ + 'success' => false, + 'message' => $data['return']['message'] ?? 'خطا در بررسی وضعیت', + ]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(), + ]; + } + } +} diff --git a/04_Laravel/app/Services/PdfService.php b/04_Laravel/app/Services/PdfService.php index 8320d53..314f214 100644 --- a/04_Laravel/app/Services/PdfService.php +++ b/04_Laravel/app/Services/PdfService.php @@ -87,6 +87,194 @@ class PdfService return $this->generatePdf($html, 'A5', 'landscape'); } + /** + * تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice + */ + public function importInvoice(Shipment $shipment, array $options = []): string + { + $shipment->loadMissing(['fromCountry', 'toCountry', 'items']); + + // آدرس گیرنده + $receiver = [ + 'name' => $shipment->receiver_name, + 'company' => $shipment->receiver_company, + 'phone' => $shipment->receiver_phone, + 'email' => $shipment->receiver_email, + 'address' => $shipment->receiver_address, + 'city' => $shipment->receiver_city, + 'country' => $shipment->toCountry?->name ?? '', + ]; + + // شماره فاکتور (از شماره AWB مشتق می‌شود) + $invoice_no = $options['invoice_no'] ?? 'PEX' . date('ymd') . substr($shipment->awb_no, -4); + + // تاریخ فاکتور + $invoice_date = $options['invoice_date'] ?? now()->format('l, F j, Y'); + + // دفتر گمرکی + $customs_office = $options['customs_office'] ?? 'Imam Khomeini Airport'; + + // نرخ ارز + $exchange_rate = $options['exchange_rate'] ?? 500000; // پیش‌فرض + + // اقلام خدماتی (۱۰ ردیف ثابت مطابق Sheet ENG Invoice) + $service_items = $this->buildServiceItems($shipment, $options); + + // جمع‌بندی + $freight_items = array_slice($service_items, 0, 5); // آیتم‌های حمل + $clearance_items = array_slice($service_items, 5); // آیتم‌های ترخیص + + $freight_subtotal_currency = array_sum(array_column($freight_items, 'total_currency')); + $freight_subtotal_rial = array_sum(array_column($freight_items, 'total_rial')); + $clearance_subtotal_currency = array_sum(array_column($clearance_items, 'total_currency')); + $clearance_subtotal_rial = array_sum(array_column($clearance_items, 'total_rial')); + $total_currency = $freight_subtotal_currency + $clearance_subtotal_currency; + $total_rial = $freight_subtotal_rial + $clearance_subtotal_rial; + + // شرایط و ضوابط + $terms_and_conditions = $options['terms_and_conditions'] ?? + 'This proforma invoice is issued based on the shipment weight declared by the customer. Final weight and dimensions will be determined after re-weighing at the destination customs.'; + + $data = [ + 'shipment' => $shipment, + 'receiver' => $receiver, + 'invoice_no' => $invoice_no, + 'invoice_date' => $invoice_date, + 'customs_office' => $customs_office, + 'exchange_rate' => $exchange_rate, + 'service_items' => $service_items, + 'freight_subtotal_currency' => $freight_subtotal_currency, + 'freight_subtotal_rial' => $freight_subtotal_rial, + 'clearance_subtotal_currency' => $clearance_subtotal_currency, + 'clearance_subtotal_rial' => $clearance_subtotal_rial, + 'total_currency' => $total_currency, + 'total_rial' => $total_rial, + 'terms_and_conditions' => $terms_and_conditions, + ]; + + $html = view('pdfs.import_invoice', $data)->render(); + + return $this->generatePdf($html, 'A4', 'portrait'); + } + + /** + * ساخت اقلام خدماتی فاکتور واردات + */ + private function buildServiceItems(Shipment $shipment, array $options): array + { + // فیلدهای پیش‌فرض از گزینه‌ها یا مقادیر پیش‌فرض + $fields = [ + 'international_freight' => $options['international_freight'] ?? ($shipment->shipping_price ?? 0), + 'pick_up_fee' => $options['pick_up_fee'] ?? ($shipment->domestic_pickup ?? 0), + 'brand_fee' => $options['brand_fee'] ?? 0, + 'report_fee' => $options['report_fee'] ?? 0, + 'other_charges' => $options['other_charges'] ?? ($shipment->extra_service ?? 0), + 'customs_clearance' => $options['customs_clearance'] ?? 0, + 'domestic_transport' => $options['domestic_transport'] ?? ($shipment->domestic_delivery ?? 0), + 'warehousing_fee' => $options['warehousing_fee'] ?? ($shipment->warehousing_cost ?? 0), + 'order_registration_fee' => $options['order_registration_fee'] ?? 0, + 'other_clearance_charges' => $options['other_clearance_charges'] ?? 0, + ]; + + $exchange_rate = $options['exchange_rate'] ?? 500000; + $weight = $shipment->chargeable_weight ?? $shipment->weight; + + return [ + // آیتم‌های حمل (Freight) + [ + 'description' => 'International Freight', + 'unit' => 'kg', + 'quantity' => $weight, + 'unit_price' => $fields['international_freight'] > 0 ? $fields['international_freight'] / $weight : 0, + 'total_currency' => $fields['international_freight'], + 'total_rial' => $fields['international_freight'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Pick Up Fee', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['pick_up_fee'], + 'total_rial' => $fields['pick_up_fee'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Brand Fee', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['brand_fee'], + 'total_rial' => $fields['brand_fee'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Report Fee', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['report_fee'], + 'total_rial' => $fields['report_fee'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Other Charges', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['other_charges'], + 'total_rial' => $fields['other_charges'] * $exchange_rate, + 'notes' => '', + ], + // آیتم‌های ترخیص (Clearance) + [ + 'description' => 'Customs Clearance', + 'unit' => 'service', + 'quantity' => $weight, + 'unit_price' => $fields['customs_clearance'] > 0 ? $fields['customs_clearance'] / $weight : 0, + 'total_currency' => $fields['customs_clearance'], + 'total_rial' => $fields['customs_clearance'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Domestic Transport', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['domestic_transport'], + 'total_rial' => $fields['domestic_transport'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Warehousing Fee', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['warehousing_fee'], + 'total_rial' => $fields['warehousing_fee'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Order Registration Fee', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['order_registration_fee'], + 'total_rial' => $fields['order_registration_fee'] * $exchange_rate, + 'notes' => '', + ], + [ + 'description' => 'Other Clearance Charges', + 'unit' => '', + 'quantity' => 0, + 'unit_price' => 0, + 'total_currency' => $fields['other_clearance_charges'], + 'total_rial' => $fields['other_clearance_charges'] * $exchange_rate, + 'notes' => '', + ], + ]; + } + /** * تولید بارکد از AWB number — base64 embed (مطمئن‌ترین روش) */ diff --git a/04_Laravel/app/Traits/Auditable.php b/04_Laravel/app/Traits/Auditable.php new file mode 100644 index 0000000..6a593a2 --- /dev/null +++ b/04_Laravel/app/Traits/Auditable.php @@ -0,0 +1,37 @@ +getAttributes(), "ایجاد رکورد جدید"); + }); + + static::updated(function (Model $model) { + $changes = $model->getChanges(); + $original = $model->getOriginal(); + + // حذف فیلدهای زمانی از تغییرات + unset($changes['updated_at'], $original['updated_at']); + + if (!empty($changes)) { + AuditLog::log('updated', $model, $original, $changes, "به‌روزرسانی رکورد"); + } + }); + + static::deleted(function (Model $model) { + AuditLog::log('deleted', $model, $model->getAttributes(), [], "حذف رکورد"); + }); + } + + public function getAuditableChanges(): array + { + return $this->getChanges(); + } +} diff --git a/04_Laravel/config/services.php b/04_Laravel/config/services.php index 6a90eb8..eaa9dd6 100644 --- a/04_Laravel/config/services.php +++ b/04_Laravel/config/services.php @@ -35,4 +35,9 @@ return [ ], ], + 'kavenegar' => [ + 'api_key' => env('KAVENEGAR_API_KEY', ''), + 'sender' => env('KAVENEGAR_SENDER', '10008566'), + ], + ]; diff --git a/04_Laravel/database/migrations/2026_09_03_000001_add_approval_statuses_to_shipments_table.php b/04_Laravel/database/migrations/2026_09_03_000001_add_approval_statuses_to_shipments_table.php new file mode 100644 index 0000000..3bf7f2b --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000001_add_approval_statuses_to_shipments_table.php @@ -0,0 +1,60 @@ +where('status', 'pending_payment') + ->update(['status' => 'approved']); + } + + public function down(): void + { + // تبدیل approved به pending_payment قبل از حذف + DB::table('shipments') + ->whereIn('status', ['pending_approval', 'approved']) + ->update(['status' => 'pending_payment']); + + DB::statement(" + ALTER TABLE `shipments` + MODIFY COLUMN `status` ENUM( + 'pending_payment', + 'cancelled', + 'processed', + 'picked_up', + 'in_transit', + 'out_for_delivery', + 'delivered', + 'failed', + 'returned', + 'archived' + ) DEFAULT 'pending_payment' + "); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_03_000002_create_commitment_forms_table.php b/04_Laravel/database/migrations/2026_09_03_000002_create_commitment_forms_table.php new file mode 100644 index 0000000..9ff38ce --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000002_create_commitment_forms_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->string('file_path'); + $table->string('file_type'); // pdf, png, jpg + $table->unsignedBigInteger('file_size'); + $table->enum('direction', ['export', 'import', 'both'])->default('both'); + $table->boolean('is_active')->default(true); + $table->integer('sort_order')->default(0); + $table->unsignedBigInteger('uploaded_by')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('uploaded_by')->references('id')->on('users')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('commitment_forms'); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_03_000003_add_credit_fields_to_users_table.php b/04_Laravel/database/migrations/2026_09_03_000003_add_credit_fields_to_users_table.php new file mode 100644 index 0000000..4fa2665 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000003_add_credit_fields_to_users_table.php @@ -0,0 +1,23 @@ +decimal('credit_limit', 15, 2, true)->default(0)->after('phone'); + $table->decimal('credit_used', 15, 2, true)->default(0)->after('credit_limit'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['credit_limit', 'credit_used']); + }); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_03_000004_create_shipment_checklists_table.php b/04_Laravel/database/migrations/2026_09_03_000004_create_shipment_checklists_table.php new file mode 100644 index 0000000..5b27fd5 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000004_create_shipment_checklists_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('shipment_id')->constrained()->onDelete('cascade'); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); // کارمند + $table->string('status'); // وضعیت فعلی سفارش + $table->boolean('is_completed')->default(false); // آیا چک‌لیست تکمیل شده + $table->text('notes')->nullable(); // یادداشت‌های کارمند + $table->timestamp('completed_at')->nullable(); // زمان تکمیل + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_checklists'); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_03_000005_create_audit_logs_table.php b/04_Laravel/database/migrations/2026_09_03_000005_create_audit_logs_table.php new file mode 100644 index 0000000..cf18c46 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000005_create_audit_logs_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // کاربری که تغییر داده + $table->string('event'); // نوع رویداد (create, update, delete, etc.) + $table->string('auditable_type'); // مدل مورد نظر + $table->unsignedBigInteger('auditable_id'); // شناسه رکورد + $table->json('old_values'); // مقادیر قبلی + $table->json('new_values'); // مقادیر جدید + $table->text('description')->nullable(); // توضیحات + $table->string('ip_address', 45)->nullable(); // آدرس IP + $table->string('user_agent')->nullable(); // مرورگر کاربر + $table->timestamps(); + + $table->index(['auditable_type', 'auditable_id']); + $table->index('event'); + $table->index('created_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('audit_logs'); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_03_000006_add_import_invoice_fields_to_shipments_table.php b/04_Laravel/database/migrations/2026_09_03_000006_add_import_invoice_fields_to_shipments_table.php new file mode 100644 index 0000000..6c4b714 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_03_000006_add_import_invoice_fields_to_shipments_table.php @@ -0,0 +1,38 @@ +decimal('brand_fee', 15, 2, true)->default(0)->after('cod_amount'); + $table->decimal('report_fee', 15, 2, true)->default(0)->after('brand_fee'); + $table->decimal('customs_clearance_cost', 15, 2, true)->default(0)->after('report_fee'); + $table->decimal('order_registration_fee', 15, 2, true)->default(0)->after('customs_clearance_cost'); + $table->decimal('other_clearance_charges', 15, 2, true)->default(0)->after('order_registration_fee'); + $table->decimal('exchange_rate', 10, 4, true)->default(0)->after('other_clearance_charges'); + $table->string('goods_nature')->nullable()->after('exchange_rate'); + $table->string('invoice_currency', 10)->default('USD')->after('goods_nature'); + }); + } + + public function down(): void + { + Schema::table('shipments', function (Blueprint $table) { + $table->dropColumn([ + 'brand_fee', + 'report_fee', + 'customs_clearance_cost', + 'order_registration_fee', + 'other_clearance_charges', + 'exchange_rate', + 'goods_nature', + 'invoice_currency', + ]); + }); + } +}; diff --git a/04_Laravel/read_excel.php b/04_Laravel/read_excel.php new file mode 100644 index 0000000..ac271d2 --- /dev/null +++ b/04_Laravel/read_excel.php @@ -0,0 +1,26 @@ +load(__DIR__ . '/../01_Documents/Sheet ENG invoice.xlsx'); +$sheet = $spreadsheet->getActiveSheet(); + +echo 'Sheet: ' . $sheet->getTitle() . PHP_EOL; +echo 'Rows: ' . $sheet->getHighestRow() . PHP_EOL; + +// Use toArray for cleaner output +$data = $sheet->toArray(null, true, true, true); + +echo PHP_EOL . '=== CONTENT ===' . PHP_EOL; + +foreach ($data as $rowIndex => $row) { + $line = ''; + foreach ($row as $colIndex => $value) { + if ($value !== null && trim((string)$value) !== '') { + $line .= $colIndex . '=' . trim((string)$value) . ' | '; + } + } + if ($line) echo 'Row' . $rowIndex . ': ' . $line . PHP_EOL; +} diff --git a/04_Laravel/resources/views/filament/pages/bulk-tracking-import.blade.php b/04_Laravel/resources/views/filament/pages/bulk-tracking-import.blade.php new file mode 100644 index 0000000..16eb46f --- /dev/null +++ b/04_Laravel/resources/views/filament/pages/bulk-tracking-import.blade.php @@ -0,0 +1,44 @@ + +
+ {{ $this->form }} + +
+ + ایمپورت وضعیت ترکینگ + +
+
+ +
+

راهنما

+
+

فرمت فایل CSV:

+
    +
  • خط اول: هدر (.awb_no,status,description)
  • +
  • هر سطر بعدی: یک سفارش
  • +
  • awb_no: شماره سفارش (الزامی)
  • +
  • status: وضعیت جدید (اختیاری - اگر خالی باشد از وضعیت پیش‌فرض استفاده می‌شود)
  • +
  • description: توضیحات (اختیاری)
  • +
+ +

وضعیت‌های مجاز:

+
+ pending_approval + approved + pending_payment + paid + processed + picked_up + in_transit + out_for_delivery + delivered + cancelled +
+ +

نمونه فایل:

+
.awb_no,status,description
+123456789,in_transit,ارسال شد از تهران
+987654321,delivered,تحویل داده شد
+
+
+
diff --git a/04_Laravel/resources/views/pdfs/import_invoice.blade.php b/04_Laravel/resources/views/pdfs/import_invoice.blade.php new file mode 100644 index 0000000..a847850 --- /dev/null +++ b/04_Laravel/resources/views/pdfs/import_invoice.blade.php @@ -0,0 +1,353 @@ + + + + + + + +
+ +
+

INVOICE TO

+

{{ $receiver['name'] }}

+

{{ $receiver['company'] ?: '' }}

+

{{ $receiver['address'] }}

+

{{ $receiver['city'] }}, {{ $receiver['country'] }}

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
Invoice No.{{ $invoice_no }}
Date{{ $invoice_date }}
Origin{{ $shipment->fromCountry?->name }}
TransportAir Freight
Customs{{ $customs_office ?: 'Imam Khomeini Airport' }}
+
+ + +
+

SHIPMENT DETAILS

+
+
+
Actual Weight:
+
{{ number_format($shipment->weight, 2) }} KG
+
+
+
Volume:
+
{{ $shipment->dimensions ?: '—' }}
+
+
+
Volumetric Weight:
+
{{ number_format($shipment->volumetric_weight ?? 0, 2) }} KG
+
+
+
Chargeable Weight:
+
{{ number_format($shipment->chargeable_weight ?? $shipment->weight, 2) }} KG
+
+
+
Goods Nature:
+
{{ $shipment->content_description ?: 'General Goods' }}
+
+
+
Exchange Rate:
+
{{ number_format($exchange_rate) }} IRR
+
+
+
+ + +
+ + + + + + + + + + + + + + + @foreach($service_items as $index => $item) + + + + + + + + + + + @endforeach + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No.Item DescriptionUnitQtyUnit PriceTotal (Currency)Total (Rial)Notes
{{ $index + 1 }}{{ $item['description'] }}{{ $item['unit'] ?: 'service' }}{{ number_format($item['quantity'], 2) }}{{ $item['unit_price'] > 0 ? number_format($item['unit_price'], 2) : '0.00' }}{{ $item['total_currency'] > 0 ? number_format($item['total_currency'], 2) : '0.00' }}{{ $item['total_rial'] > 0 ? number_format($item['total_rial']) : '0' }}{{ $item['notes'] ?: '' }}
Freight Subtotal{{ number_format($freight_subtotal_currency, 2) }}{{ number_format($freight_subtotal_rial) }}
Clearance Subtotal{{ number_format($clearance_subtotal_currency, 2) }}{{ number_format($clearance_subtotal_rial) }}
Currency Rate:{{ number_format($exchange_rate) }}
TOTAL{{ number_format($total_currency, 2) }}{{ number_format($total_rial) }}
+
+ + +
+

TERMS & CONDITIONS

+

{{ $terms_and_conditions }}

+
+ + +
+
+
Authorised Signature
+
IFNEX Logistics
+
+
+ + + +
+ + diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php index 8303a9d..9386452 100644 --- a/04_Laravel/routes/api.php +++ b/04_Laravel/routes/api.php @@ -1,10 +1,14 @@ prefix('v1')->group(function () { Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']); Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway']); }); + + // ─── Staff Orders API (تأیید سفارشات) ─── + Route::prefix('staff')->group(function () { + // لیست سفارشات در انتظار تأیید + Route::get('/orders/pending-approval', [StaffOrderController::class, 'pendingApproval']); + + // تأیید یا رد سفارش + Route::post('/orders/{shipment}/approve', [StaffOrderController::class, 'approve']); + Route::post('/orders/{shipment}/reject', [StaffOrderController::class, 'reject']); + + // ─── Customer Financial API (وضعیت مالی مشتری) ─── + Route::get('/customers/search', [CustomerFinancialController::class, 'search']); + Route::get('/customers/{customer}/financial-status', [CustomerFinancialController::class, 'financialStatus']); + }); }); // ══════════════════════════════════════════════════════════════ @@ -95,4 +113,15 @@ Route::any('/v1/payment/callback', [PaymentController::class, 'callback']) // ══════════════════════════════════════════════════════════════ Route::post('/v1/calculate', [PricingController::class, 'calculate']); Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']); -Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']); \ No newline at end of file +Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']); + +// API فایل‌های تعهدنامه (عمومی) +Route::get('/v1/commitment-forms', [CommitmentFormController::class, 'index']); +Route::get('/v1/commitment-forms/{direction}', [CommitmentFormController::class, 'byDirection']); + +// API تأیید موبایل (عمومی) +Route::post('/v1/verify/send-code', [MobileVerificationController::class, 'sendCode']); +Route::post('/v1/verify/check-code', [MobileVerificationController::class, 'checkCode']); + +// API ارسال پیامک (نیاز به auth دارد) +Route::middleware(['auth:sanctum'])->post('/v1/verify/send-sms', [MobileVerificationController::class, 'sendSms']); \ No newline at end of file diff --git a/04_Laravel/routes/web.php b/04_Laravel/routes/web.php index a1629d7..b5e6930 100644 --- a/04_Laravel/routes/web.php +++ b/04_Laravel/routes/web.php @@ -19,6 +19,7 @@ Route::middleware('auth')->group(function () { Route::get('/shipments/{shipment}/pdf/awb', [ShipmentPdfController::class, 'awb'])->name('shipments.pdf.awb'); Route::get('/shipments/{shipment}/pdf/invoice', [ShipmentPdfController::class, 'invoice'])->name('shipments.pdf.invoice'); Route::get('/shipments/{shipment}/pdf/label', [ShipmentPdfController::class, 'label'])->name('shipments.pdf.label'); + Route::get('/shipments/{shipment}/pdf/import-invoice', [ShipmentPdfController::class, 'importInvoice'])->name('shipments.pdf.import-invoice'); }); // صفحات نتیجه پرداخت diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..561a243 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,259 @@ +# IFNEX Logistics Management System — Agent Guide + +> **هدف:** راهنمای جامع برای دستیار هوش مصنوعی (OpenCode + DeepSeek Flash) جهت درک سریع پروژه، معماری، قراردادها و نکات کلیدی. +> +> **نسخه:** 1.0 +> **تاریخ:** 2026-09-02 +> **مدل پیشنهادی:** DeepSeek Flash (تنظیم شده با API) + +--- + +## 🎯 خلاصه پروژه + +سیستم مدیریت لجستیک بین‌المللی با معماری Headless (Laravel 11 به‌عنوان بک‌اند، WordPress به‌عنوان فرانت‌اند، Filament 3.3 به‌عنوان پنل مدیریت). جایگزین فرآیندهای دستی مبتنی بر اکسل شده و از Multi-Package، فاکتور گمرکی (Invoice)، کیف پول دیجیتال، درگاه پرداخت Zarinpal، تولید PDF با بارکد، و سیستم رهگیری پیشرفته پشتیبانی می‌کند. + +**وضعیت فعلی:** فازهای ۰، ۱، ۲، ۳ و ۳.۵ (۹۰٪) تکمیل شده‌اند. تنها مورد باقیمانده از فاز ۳.۵، پشتیبانی کامل چندزبانه (i18n) است که به زمان دیپلوی موکول شده است. + +--- + +## 🧰 تکنولوژی‌ها و نسخه‌ها + +| لایه | تکنولوژی | نسخه / توضیح | +|------|-----------|--------------| +| **Backend** | Laravel | 11.x | +| **PHP** | PHP | ^8.2 (۸.۳ نیز پشتیبانی می‌شود) | +| **Admin Panel** | Filament | 3.3.x | +| **Frontend** | WordPress | 7.0.3 + قالب سفارشی IFNEX | +| **Authentication** | Laravel Sanctum + Bridge Auth | بدون رمز عبور (API Key مشترک) | +| **Payment** | Zarinpal + Mock Gateway (تست) | کلید sandbox برای تست لوکال | +| **PDF & Barcode** | Dompdf + picqer/php-barcode-generator | تولید AWB، Invoice، Label | +| **Excel** | maatwebsite/excel | Import/Export نرخ‌ها و داده‌های تاریخی | +| **Date/Time** | morilog/jalali + Carbon | تاریخ شمسی در نمایش، میلادی در DB | +| **Database** | MySQL | 8.0+ | +| **Queue** | Redis (ترجیح) / Database | برای پردازش‌های سنگین | +| **Server** | HestiaCP + Nginx + PHP-FPM | تولید (api.ifnex.vernahost.ir) | + +--- + +## 📂 ساختار دایرکتوری (کلیدی) + +IFNEX-Logistics/ +├── 01_Documents/ # مستندات فنی (تحلیل اکسل، نقشه راه، چک‌لیست، ...) +│ ├── EXCEL_ANALYSIS.md # تحلیل کامل فایل‌های اکسل (۳۹۵۰ رکورد) +│ ├── IFNEX_Phase0_Checklist.md # چک‌لیست کامل فازها +│ ├── IFNEX_Roadmap.md # نقشه راه ۵ فازی +│ ├── IFNEX_File_Map.md # نقشه ۱۰۰+ فایل پروژه +│ ├── IFNEX_I18N_Strategy.md # استراتژی چندزبانه +│ ├── IFNEX_Commercial_Model.md # مدل تجاری و پلن‌های فروش +│ └── DESIGN_SYSTEM.md # رنگ‌ها، تایپوگرافی، فاصله‌گذاری +│ +├── 03_WordPress/ # فرانت‌اند وردپرس +│ ├── wp-content/themes/ifnex/ # قالب سفارشی +│ └── wp-content/plugins/ifnex-bridge/ # پلاگین ارتباط با لاراول +│ +├── 04_Laravel/ # بک‌اند لاراول (هسته اصلی) +│ ├── app/ +│ │ ├── Enums/ # ShipmentStatus, ShipmentDirection, ShipmentType, PaymentGateway, TransactionStatus +│ │ ├── Filament/ +│ │ │ ├── Resources/ # ۱۰+ Resource (Shipment, Country, ShippingRate, Currency, ...) +│ │ │ ├── Pages/ # Settings, PriceTest, ImportRates, FinancialReport +│ │ │ └── Widgets/ # داشبورد +│ │ ├── Http/ +│ │ │ ├── Controllers/Api/ # Track, Pricing, Auth, Bridge, Customer, Wallet, Payment, MockGateway +│ │ │ └── Middleware/ # ApiKeyMiddleware +│ │ ├── Models/ # ۱۷ مدل (Shipment, Package, Invoice, Wallet, ...) +│ │ ├── Notifications/ # ShipmentUpdatedNotification +│ │ ├── Services/ # PriceCalculator, Pdf, Tracking, OrderPayment, Zarinpal, MockZarinpal +│ │ ├── Imports/ # OldShipmentsImport, ShippingRatesImport +│ │ └── Exports/ # ShippingRatesTemplateExport +│ ├── database/ +│ │ ├── migrations/ # ۲۸+ migration +│ │ └── seeders/ # Countries, SystemSettings, DatabaseSeeder +│ ├── resources/views/ +│ │ ├── pdfs/ # awb, invoice, label (با بارکد) +│ │ └── filament/ # صفحات سفارشی +│ └── routes/ +│ ├── api.php # ۳۰+ endpoint +│ └── web.php # روت‌های عمومی (دانلود template، Mock Gateway) +│ +├── DEPLOYMENT.md # راهنمای استقرار در سرور +└── README.md # معرفی کلی پروژه + + +--- + +## 🔑 مفاهیم کلیدی بیزینس + +### ۱. مرسوله (Shipment) +- **انواع (Type):** `DOC_NORMAL`، `DOC_ECONOMY`، `PARCEL` +- **جهت (Direction):** `export` (صادرات) و `import` (واردات) +- **وضعیت (Status):** `pending` → `confirmed` → `picked_up` → `in_transit` → `out_for_delivery` → `delivered` / `failed` / `returned` / `cancelled` +- **ویژگی‌ها:** دارای چند بسته (`packages`)، اقلام گمرکی (`items` - فقط برای PARCEL)، تاریخچه تغییرات وضعیت (`statusHistories`)، شرکت‌های حمل (`carrierMappings`)، رویدادهای رهگیری (`trackingEvents`). + +### ۲. بسته (Package) +- هر مرسوله می‌تواند چندین بسته داشته باشد. +- وزن حجمی = `(Length × Width × Height) / 5000` (استاندارد IATA). +- وزن قابل پرداخت = `max(وزن واقعی, وزن حجمی)`. + +### ۳. فاکتور گمرکی (Invoice) +- فقط برای مرسوله‌های نوع `PARCEL` صادر می‌شود. +- حداکثر ۹ قلم کالا (شرح، HS Code، تعداد، قیمت واحد، مجموع به USD). +- مجموع کل فاکتور در `shipments.invoice_total_usd` ذخیره می‌شود. + +### ۴. کیف پول (Wallet) +- هر کاربر یک کیف پول دارد. +- تراکنش‌ها (`WalletTransaction`) با نوع `deposit`، `payment`، `refund`، `adjustment`. +- پرداخت از کیف پول یا درگاه Zarinpal (با Mock برای تست). + +### ۵. احراز هویت Bridge +- کاربران وردپرس بدون نیاز به رمز عبور، از طریق `POST /api/v1/bridge/login` با ارسال `bridge_api_key` و `wp_user_id` وارد لاراول می‌شوند. +- پاسخ شامل `token` Sanctum (معتبر ۳۰ روز) و اطلاعات کاربر است. +- کلید `IFNEX_BRIDGE_API_KEY` باید در هر دو طرف (`.env` لاراول و تنظیمات پلاگین وردپرس) یکسان باشد. + +### ۶. رهگیری (Tracking) +- کاربر با شماره AWB جستجو می‌کند. +- سیستم رویدادهای رهگیری را از جدول `shipment_tracking_events` با `source` (manual, api, system) نمایش می‌دهد. +- در فاز ۳.۵، صفحه رهگیری با تایم‌لاین زیبا و badge رنگی بازطراحی شده است. + +### ۷. قیمت‌گذاری (Pricing) +- بر اساس ۴ زون مجزا (Export/Import × Parcel/Doc) و ۳ نوع سرویس. +- فرمول: قیمت پایه (AED) × ضریب سود × نرخ تبدیل به ریال + هزینه‌های جانبی (Packing, Domestic Pickup, ...) + VAT (۹٪). +- محموله‌های بالای ۳۰ کیلوگرم مشمول نرخ ویژه (Spot Rate) هستند و محاسبه آنلاین ندارند. + +--- + +## ⚙️ دستورات روزمره (برای ایجنت) + +```bash +# نصب وابستگی‌ها +composer install + +# تنظیم محیط +cp .env.example .env +php artisan key:generate + +# دیتابیس (ابتدا باید ایجاد شود) +php artisan migrate --force +php artisan db:seed --force + +# اجرای سرور توسعه +php artisan serve + +# اجرای Queue (برای پردازش‌های سنگین) +php artisan queue:work + +# تست‌ها +php artisan test + +# پاک‌سازی کش +php artisan optimize:clear +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan filament:clear-cached-components + +# ایمپورت/اکسپورت نرخ‌ها +php artisan ifnex:import-rates +php artisan ifnex:export-rates-template + +# بروزرسانی نرخ ارز (کرون) +php artisan ifnex:update-exchange-rates + +# تولید API Token برای ادمین +php artisan ifnex:generate-api-token + + +🧪 متغیرهای محیطی کلیدی (.env) +APP_ENV=local +APP_DEBUG=true +APP_URL=http://localhost:8000 + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=ifnex_db +DB_USERNAME=root +DB_PASSWORD= + +# IFNEX اختصاصی +IFNEX_API_KEY=ifnex-local-dev-key +IFNEX_BRIDGE_API_KEY=ifnex-bridge-secret-key-2026-vernasoft # باید با وردپرس یکی باشد +IFNEX_TRACKING_RATE_LIMIT=60 + +# CORS (فقط دامنه‌های مجاز وردپرس) +CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1 + +# Zarinpal (برای تست از Mock استفاده کن) +ZARINPAL_MERCHANT_ID=fake-merchant-id-for-testing # اگر fake باشد، MockGateway فعال می‌شود +ZARINPAL_SANDBOX=true +ZARINPAL_CALLBACK_URL=http://localhost:8000/api/v1/payment/callback +ZARINPAL_FRONTEND_SUCCESS_URL=http://localhost/IFNEX-Logistics/03_WordPress/wallet +ZARINPAL_FRONTEND_FAILURE_URL=http://localhost/IFNEX-Logistics/03_WordPress/wallet + +# Wallet +WALLET_MIN_DEPOSIT=10000 +WALLET_MAX_DEPOSIT=500000000 +WALLET_AUTO_CREATE=true + + +⚠️ خط قرمزها (ممنوعیت‌های مطلق) + +❌ هرگز ✅ همیشه +برگرداندن کشورها به ۲ زون ۴ زون مجزا (Export/Import × Parcel/Doc) +استفاده از ۲ نوع سرویس ۳ نوع (DOC_NORMAL, DOC_ECONOMY, PARCEL) +ذخیره تاریخ شمسی در DB ذخیره timestamp میلادی + تبدیل در نمایش +CORS * در Production CORS محدود به دامنه وردپرس +کامیت .env در Git در .gitignore باشد +APP_DEBUG=true در Production APP_DEBUG=false +PDF فارسی (AWB/Invoice/Label) همیشه انگلیسی (برای حمل بین‌المللی) +کپی از DHL طراحی منحصر به فرد IFNEX +استفاده از wire:click برای دانلود استفاده از
با روت مستقیم +getFormActions() در Custom Pages استفاده از wire:click در Blade +هدایت AJAX به redirect() استفاده از payment_url در response JSON +متدهای نوتیفیکیشن بیرون از کلاس داخل کلاس IFNEX_User_Bridge +📌 نکات ویژه برای ایجنت (DeepSeek Flash) +هنگام تولید کد جدید، حتماً از Enum‌ها به جای رشته‌های سخت‌کد شده استفاده کن. + +برای هر مدل جدید، migration، مدل، و در صورت نیاز کنترلر/ریسورس Filament بساز. + +خطاهای رایج: + +عدم وجود فیلد deleted_at در کوئری‌ها (از SoftDeletes استفاده کن). + +فراموشی fillable یا casts در مدل‌ها. + +فراموشی $with برای بارگذاری روابط در Resourceها. + +استفاده از redirect() در کنترلرهای API (باید JSON برگردانند). + +برای تغییر وضعیت مرسوله، حتماً تایم‌لاین را به‌روز کن (مدل ShipmentStatusHistory). + +در فرم سفارش مشتری، مرحله Invoice فقط برای نوع PARCEL نمایش داده شود. + +تولید PDF با بارکد به‌صورت base64 embed انجام شود. + +Bridge Auth نیازی به رمز عبور ندارد؛ فقط از IFNEX_BRIDGE_API_KEY استفاده می‌کند. + +تست لوکال پرداخت: از ZARINPAL_MERCHANT_ID=fake-merchant-id-for-testing برای فعال‌سازی Mock Gateway استفاده کن. + +مدیریت تاریخ: همیشه از Carbon استفاده کن و تاریخ را به‌صورت Y-m-d H:i:s در DB ذخیره کن. + +برای کوئری‌های سنگین، از chunk() یا cursor() استفاده کن تا حافظه مصرف نشود. + + +📚 مستندات مرجع (برای مطالعه بیشتر) +فایل محتوا +01_Documents/EXCEL_ANALYSIS.md تحلیل کامل فایل‌های اکسل (۳۹۵۰ رکورد، فرمول‌ها، زون‌ها) +01_Documents/IFNEX_Phase0_Checklist.md چک‌لیست کامل فازها (۰ تا ۳.۵) +01_Documents/IFNEX_Roadmap.md نقشه راه ۵ فازی +01_Documents/IFNEX_File_Map.md نقشه ۱۰۰+ فایل پروژه +01_Documents/IFNEX_I18N_Strategy.md استراتژی چندزبانه (برای زمان دیپلوی) +01_Documents/IFNEX_Commercial_Model.md مدل تجاری و پلن‌های فروش +01_Documents/DESIGN_SYSTEM.md رنگ‌ها، تایپوگرافی، فاصله‌گذاری +DEPLOYMENT.md راهنمای کامل استقرار روی سرور +04_Laravel/README.md راهنمای بک‌اند +تاریخ: 2026-09-02 +نسخه: 1.0 +تهیه‌کننده: VernaSoft Group — Kazem Alghasi +مدل هدف: DeepSeek Flash (API) + diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index aafa83e..0033cd8 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,6 +1,6 @@ # راهنمای استقرار IFNEX -> **آخرین بروزرسانی:** 2026-08-29 +> **آخرین بروزرسانی:** 2026-09-03 --- @@ -8,8 +8,8 @@ | محیط | دامنه | نقش | |-------|-------|------| -| Production | api.ifnex.vernahost.ir | API لاراول | -| Production | ifnex.vernahost.ir | وب‌سایت وردپرس | +| Production | system.ifnex.ir | API لاراول | +| Production | ifnex.ir | وب‌سایت وردپرس | | Local | localhost:8000 | توسعه | --- @@ -61,7 +61,7 @@ nano .env ```env APP_ENV=production APP_DEBUG=false -APP_URL=https://api.ifnex.vernahost.ir +APP_URL=https://system.ifnex.ir DB_HOST=localhost DB_DATABASE=ifnex_db @@ -71,14 +71,18 @@ DB_PASSWORD=STRONG_PASSWORD # مهم: Bridge API Key باید با وردپرس یکسان باشد IFNEX_BRIDGE_API_KEY=ifnex-bridge-secret-key-2026-vernasoft -CORS_ALLOWED_ORIGINS=https://ifnex.vernahost.ir +CORS_ALLOWED_ORIGINS=https://ifnex.ir # Payment Gateway ZARINPAL_SANDBOX=false ZARINPAL_MERCHANT_ID=YOUR_REAL_MERCHANT_ID -ZARINPAL_CALLBACK_URL=https://api.ifnex.vernahost.ir/api/v1/payment/callback -ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.vernahost.ir/wallet -ZARINPAL_FRONTEND_FAILURE_URL=https://ifnex.vernahost.ir/wallet +ZARINPAL_CALLBACK_URL=https://system.ifnex.ir/api/v1/payment/callback +ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.ir/wallet +ZARINPAL_FRONTEND_FAILURE_URL=https://ifnex.ir/wallet + +# Kavenegar SMS Service +KAVENEGAR_API_KEY=YOUR_KAVENEGAR_API_KEY +KAVENEGAR_SENDER=10008566 ``` ### ۱.۴ دیتابیس diff --git a/README.md b/README.md index afab767..bdb4b9c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ - **اعلان‌های دیتابیس** برای ادمین و مشتری با badge unread در سایدبار - **پشتیبانی از ۱۹۲ کشور** با پیش‌شماره تلفن استاندارد ISO 3166-1 - **اعتبارسنجی فرم‌ها** شامل تشخیص خودکار زبان فارسی/انگلیسی و پیش‌شماره خودکار +- **سیستم تأیید سفارش** با فلوی `pending_approval → approved → paid → processed` +- **فرم تعهدنامه** با قابلیت آپلود فایل توسط مدیر و مشاهده توسط مشتری +- **فاکتور واردات** با طراحی مطابق Sheet ENG Invoice +- **مانده حساب** با نمایش موجودی کیف پول منهای بدهی‌ها +- **وضعیت مالی مشتری** برای مدیر/کارمند با جزئیات بدهی به هر ارز +- **سیستم اعتباردهی (Credit)** با قابلیت افزایش/کاهش توسط مدیر کل +- **چک‌لیست کارمند** برای هر سفارش + تاریخچه مراحل +- **ایمپورت گروهی وضعیت ترکینگ** با فایل CSV +- **سیستم Audit Log** برای لاگ تغییرات مدیر +- **اتصال سرویس اس‌ام‌اس Kavenegar** برای تأیید موبایل و ارسال پیامک ---