From 8802f08d4f044c42148865fc4a98bb199ada20c4 Mon Sep 17 00:00:00 2001 From: Ricardo Mancinas Date: Thu, 23 Jul 2026 23:20:41 -0700 Subject: [PATCH] feat(reports): reports module + /inicio + edo-cuenta-datos prefill - New reports backend (registry, service, controller, outputs, types) with catalog endpoint + slug/CSV/XLSX/PDF/print outputs. - /reportes catalog + /reportes/[slug] runner; ReportRunner + ContextReports components wire pre-filtered links from domain pages. - Fix: /reportes/[slug] now reads searchParams and forwards initialParams to ReportRunner so /reportes/edo-cuenta-datos?customerId=... auto-runs instead of dropping the id and forcing a manual customer search. - /inicio landing page; root + login redirect to /inicio. - Company header env vars + logo asset for PDF/print rendering. - exceljs + pdfkit deps. --- .env.example | 13 + apps/api/assets/company_logo.png | Bin 0 -> 112604 bytes apps/api/package.json | 3 + apps/api/src/app.module.ts | 2 + apps/api/src/reports/company.ts | 103 +++ apps/api/src/reports/outputs.ts | 433 ++++++++++ apps/api/src/reports/reports.controller.ts | 130 +++ apps/api/src/reports/reports.module.ts | 9 + apps/api/src/reports/reports.registry.ts | 763 +++++++++++++++++ apps/api/src/reports/reports.service.ts | 45 + apps/api/src/reports/reports.types.ts | 140 +++ apps/web/src/app/banco/page.tsx | 8 + apps/web/src/app/clientes/[id]/page.tsx | 10 + apps/web/src/app/clientes/page.tsx | 7 + apps/web/src/app/estado-cuenta/page.tsx | 10 + apps/web/src/app/globals.css | 485 +++++++++++ apps/web/src/app/inicio/page.tsx | 455 ++++++++++ apps/web/src/app/login/page.tsx | 4 +- apps/web/src/app/page.tsx | 2 +- apps/web/src/app/polizas/[id]/page.tsx | 10 + apps/web/src/app/polizas/page.tsx | 8 + apps/web/src/app/reportes/[slug]/page.tsx | 110 +++ apps/web/src/app/reportes/page.tsx | 100 +++ apps/web/src/app/servicios/page.tsx | 8 + apps/web/src/components/AppShell.tsx | 10 +- apps/web/src/components/ContextReports.tsx | 57 ++ apps/web/src/components/ReportRunner.tsx | 476 +++++++++++ apps/web/src/lib/api.ts | 36 + apps/web/src/lib/types.ts | 55 ++ pnpm-lock.yaml | 938 +++++++++++++++++++++ 30 files changed, 4424 insertions(+), 6 deletions(-) create mode 100644 apps/api/assets/company_logo.png create mode 100644 apps/api/src/reports/company.ts create mode 100644 apps/api/src/reports/outputs.ts create mode 100644 apps/api/src/reports/reports.controller.ts create mode 100644 apps/api/src/reports/reports.module.ts create mode 100644 apps/api/src/reports/reports.registry.ts create mode 100644 apps/api/src/reports/reports.service.ts create mode 100644 apps/api/src/reports/reports.types.ts create mode 100644 apps/web/src/app/inicio/page.tsx create mode 100644 apps/web/src/app/reportes/[slug]/page.tsx create mode 100644 apps/web/src/app/reportes/page.tsx create mode 100644 apps/web/src/components/ContextReports.tsx create mode 100644 apps/web/src/components/ReportRunner.tsx diff --git a/.env.example b/.env.example index f71d455..d5c3b24 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,16 @@ DATABASE_URL=mysql://jorgecuadros:jorgecuadros@localhost:3306/jorgecuadros SESSION_SECRET=change-me-to-a-random-string WEB_ORIGIN=http://localhost:3000 NEXT_PUBLIC_API_ORIGIN=http://localhost:3001 + +# Company info — printed in the header of every report (PDF + browser +# print). Leave blank to use the placeholders. COMPANY_LOGO_PATH is +# optional; when unset the API falls back to apps/api/assets/company_logo.png. +COMPANY_NAME=Jorge Cuadros & Asociados +COMPANY_ADDRESS_LINE1= +COMPANY_ADDRESS_LINE2= +COMPANY_CITY_STATE= +COMPANY_PHONE= +COMPANY_EMAIL= +COMPANY_TAX_ID= +COMPANY_WEBSITE= +COMPANY_LOGO_PATH= diff --git a/apps/api/assets/company_logo.png b/apps/api/assets/company_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b591e6ee79c08378527c6ee9cb6150650ac54d8e GIT binary patch literal 112604 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

q>U$Tt#77B zmos4Y5nneMFcgrZiL@S^$!e1S2om>KCUH)pVzYN@+JLYtBrGrPIK%fjG0@SFjA&bR z(~or0)*I~AuvT0$WI>`qZITa>tgZ(*=3s4cOaW<Ts(jOC{7W&H5)aOV!{C4 zpBqojET)t8xbCJIPQj?&SPZF^)WTVAC{bYy5AulVC*2VC%kZ(lAoiO#_ue6g;d9t_ z|6Rs0tGNJ6=vIHc{d&z5H~Dq zGgLVEXhR_y=+nc7KJs&oNpKeCUe(3M_uze@@v9ml0@S?L7!GIAH?#TTF9O|i>hzDU zixPuwJ?!)Mgu+-C-4R0QrkK@8Ep$p6n4FnTGVtKvPR}7;FmA7hW~scK_%WQHi22vm z_Nj*h<5SWkAYzt{#p&1Zb}A!LcgH@cj9AGrf^T}q2+J?(ZTWMm!2mkVvQOLGU=2NI z{TMJrF+cN=BzCfH!!bW7?&12>zDR#yUW$n_)JmFF>xqJo$y`F4IQnGbn8fP^x@M<~ zpf)P6M%25_nfxGfk%n9Nt0FPI{^U?R%L^BT+7gsNb-QjUcc;mWBxx#i+jM_#u2Ual z`l^n4sp-b|NUa|!CO(kJtw8K?e2I0&1ZgPv~1FKv#Bo~fli;3E`eG%1AT16 z^Bek!@YY07!y78LL74~8tw1Y#LB&^tF<{8 z=#$ssWhK*~>phQpGOXI3TynCIX6+i&@Qn0@@$KdgWBg5UC6uwJe8hjxHA zSgF3C$tw$#1LWi+>-gVfWjq{1^z7i=`VZ>zBm(NJlReH8UV~4uh(w7)s@6?sz}D}d zbb8%Bga0!$*&Iyu$!rg{ZADVFzcfSYvzY=|&Oe(1p#ZXWAR*w7-v%T>VqZb*)WUs8 zSToKQIHme_VP0iHH;ul-H*82QRu6-%vn-HsOu8*#{&z?K6?hu}>i-|+cYrfG7dWqv z5}f+o!1+)OFmsZKc+ezN;HanVx%dqCtAeWS7J%49SuWrTY99rX1B}mrMMCh&0C@n8 zaS&excbFX|9Y5sUr&e(MT`7PNboGw{0#FSxl)1P9ZzKH|x&XhUIrsIf9U&3`Tl=To z0kb+BL%O(@-CsfMgHd)n0?hxCEK@8Qm?-5u(#4|e!CYb=REL{Y;Iydt2x|tWS7w29 z(Y{<0z>%u=?EnsvoPvekZXw(a9 z011MYKz89wb195`5z~3dh7ElhKqwMTR~aP5=M5Rgq$H++DRk_w)}Pw)k1hGX`3zeB zWT}ASZ&T>Q51C`na?|lPzu4u9y*gW>!hMPs#@;dQw?1|C8`azR^!eqVOZ{RE(l8EK zBFLHCQIu$JVuw7ACN>>^Gtm6h^NmNS+WM(z>_0Q#YB+uZZGF=bv+FFf-c6WO{ey4! zcLUZi&_^g|3WHj!=7(zD+5anQ-SlZI6xf2OXvI*&@k0`8EPt#2uLHU22j7O3Ff;-J z-SlZ|5%I68(E0u7Y})^JP=6h!;Q}@V5>xxd9}Erf zzhlZBfl64=L`28Ee>H**V=b46aL|l7z_r#vSNETWG1+bBT*Ggw`6)xCw%#y$S{#TV{4$+kA%a((k`DU{$WW-w3KZU72zKt*xzX zM7{9$dWUIO<@T3N;i@0}{&r94C{35WTckcQ)&%1(9cMk5R$F!r#kRkQPq+RFtxEW( zsovfHxT?R#(|*586C;SbKh0gTGn3D52^DYL<>VYaushAgVetcZ2`%+I-VR{CgfWi@4|!B2!alHou)83${R+0jY3*ey|jri;!hI*R7swBM^pQ-|{}&fkfu z7{ff-)*1~y^6~B1lpHeeSrT3{PjWbrSS%Uuf^y5W8c7oDvA(_-p|uORX6$k6fp=a% z&p?)vZ5OuA_nI_blv+M2`R$uZyO%hQ>UBd~@E?r6Ug1jo_{g8Nx6$?xGi+M1=Uo9S z6dO+NwCnYW9EAMsxtq3?W{y;B&lRTZY}@j7i!Cc1%?s2uy`En5NkH-yX*Kbz+x1%O z{%m@QE1Se}n~q9uz1?dStm#X$zko3jv!qJ&732BnSN&|66PMg`(!lvuvR++ zBR<-odevU9edlb9o=G-G-WQ*Xm^+CL>$x}6v)2>t9JBF=2dtG(V2!r7_Vnr}+pjHh z7j74wgz56HCYqT*Rp&70XnfDQ_f&L!(s?(w%`#R6(aXLaz{NsJ7Sc-oc-U9yEPM|i z5CilYpPa7rW;$SNu2AS7hJscCllKp zVfgymjYqp{w`TDyPH?K~Celerx3RHr&$BrouK?|)F(c%(gA@AO)}}mSiS(StP|`_L zlJyc|kSu~_N+X+W!5M;mTmKGi+VySrpKNk%g_ZfV53NnhG|VupPRcY)=&X**G$7+w zzsWQVjjZmK)K8;cdK@b0qFr?A0D-Wx1)w!XJGmE#Xrdj-u7dv#Oa*xE0!(Eb18cr# zWy%OkKBgi&o+aP2qCf}OpUN5V@kojBsU5iaZh-oY8bBJI_XgCF7^iBNG2LnXPDm3y ze>)C<>O==}0O-29P%dDIzLe50(-1ra;*Pe#$~dAgW!cnQN(B89AbLO;(KZM>LI9Rw zsY@W*D8k4S4g7wPi9KEl%dj5>%+a&SO>#YOKW zfgMu_K0ri-(*=w11SagJSgeTC8nqTqdL2CuD?79&1TJL(iX%d)z66RRx%oUW(bb6~ z{+CT-_8p#Bm2TPzl|7UNx@z}+HLBc@f4Red=md>wbj~{XA>h3rbEYM8reSiXAsD6g zK@?T*&Ot9vqJrd8G_Wgbc76B$nOuc)z?luzh}J-fbys*|nrbyLj>bq=wszO#ckPH( zW*i@8xWS#VyG+ug{RSnxG1LuRW)|;X{>Y=K7b{f(9}#K7N3NSzuK4PT0Zkj9RDW#S zSW~UZ5U$&`5p4<~QQiULFyt!aZ!zJsjzmvX;tqQ-s3u%f$sq?4Q-F~v9`VG!RTxQU zN80{%bcYzy|Ld!;F9SPuP*|-!e&q@H&G!7dyd|su*Jb?w0Kw&*fJH?;Is=k_^n+Sw z-o(U^)1`JDJCmh7V0Hh>$m&Dghh)F~ozTtT@D9Io`{~(j{~7;2*W&NXGnh#bklo-i z-w9&T{(9|7`S0AoQ~mIRyo^bLaiWdyf?6tZLN}0OAfs#v2`mH}n=a(EyXbs0(XrZe zaEMPJCEW+PX>aBTxoQ3opbfE=K;T*Qk=__&bMp1FCel&Y0UUxw87@+T6G=DW{MwW{ z3ATT>010I9#3@sfhPB7w9#>I-6Y3wjjzP-6>Oe+W!>Sq7JAuN?Ly8Vy>qi7mNOPtU zY_$(1WS_CyfIKv}4&X#&0_U|boX9luNX+<*L^+E#$Xa{;1CWlW{5_$p>0oc}orq#E zX2#1>`0vSNzvq?B_<4Iu1XStdR2`X4Gxz?H)AqL{Hjzsu>uu<91)l={%{HsOzvr&~ z8BFPGcI#Kcd4OfnpWz+|8EtqK22#aLdOv|wv0|%1*6f|4g5*J&!@hVmD=z=}WnD)u z|C!Ue4qW~d*LCf={E_M)kHvq;LD!ZGRF$ebni;ecsjJrzwByf{KM5!y@7)KL zB;L6KLR~v=jezpZvJw#-Up%Ye>bW32!%$TxH)w~Qr=VZvkaQ@QRT+Yfkq$!YAn+9) zXU~O0+-BWBDexiDk7jihh`$lv{MD{YJ^yM+{|kfgECNDd0z?Y37akIm0e!FsnqZ0} z^;w*ZJiDB1H)Rid=YnJDDHCkhybCXTwN9P(DQKj&Zg1>&5-Z;7k3pl7pH@L`D;H@tBBw{(U-3E-S)9?I%u1YUqamOejIzfO7Xjy0C|K5^ywS1glVZtelz zQ-`D#crYjPXt^vv&7hXP{B z2_zt|BCvxNo(2S0>Oh()dSLCgFCOO^0q7(pfND}x?;}JL;wNCLGF>?fSUpD&Yp0>N z2_I22N-AU9B{Q!8H)s{$o~-qE@HAnnM46WR1764W)z`r9ZsUu0w*mVr7Ofs2t(HAP z;ZVgVptJNFhh(J`G~{X8{1%?_bJBhjUGVa6QcO)odEeGIN(}h_=7VW4rH*gI8WTWx z$X_`X-I)8$5%82NYkv_;cJF95hr&~Q!2SdZ%HQ%c(nr;Z$?*2e?p0s^@K_??p4s-E zzgQ^&2|_Wn2q-ZBC>m=H)g6PcR*ZVIC)5HOO6jrRc_bnCp!CT5wg~yMsZIiq)n5dY z5Cc{pURPv9Af^11iE^)s0Gg@vtT^*Fq}PG#END=te>JF2eE+76{6}ukpax6bx`t{1J@kYgpg&D5Q4M2iS~{tQt7^W!2X)qT zhJiY3daZ%Kdf*Q5SC3o<{_3%lz+VkCDsXVn9cu!;KPUwRN+LiX3<80bcmi7_4D{F_ zAXkEe5g?$?2ZaNO1_1&BEAa${3K$^-T?Rk`0Hg|((nKhwm(Y^~r34U4ks$Q@2&G69 zAfS{cLMdtl2q-0hP>KYhv-i*6s#A*TNR$^H|AG(TF(X#})iP?I{WJJK_YgWDAg-98 zX7$I7TATcCOyHTEeobnXlT$n?H7D{!aoD7ZJ}+bH;9*y1qt!O%@)5#W&ZvQjWx*Ma zaNL)F7mNym^cx8$$}?Ky3pII8eEfInF5vv1RJt3Cf!_LBsvZz10T*ui4hT7o1sW~{ zp%`Is-HgHIN_n%gGbGaxS`z>)JB; zpO*Bm@Zh=*2v9xKJVW&R15mcrTJ{{!1(wHApQ4kiW@|d{?f7y@WbEB*g0}d7_mTsIPF+aQMl|VLw_`x!#!^6OY!%%?A#m|8~-$P$n z_p_i+I1RR=0xC)Gos9K-cr`Ij&jJOO6x=;(foW|C%!;(-bdh{xEbJyu5B&f*=!@%# zaU%0~fqCzG129qAP;7&jUlXB#plnObauleiA3lT!Wr(0#sTN@&uFzE^{1ghW^bWd( z2essFO_1)S_wfVRU6@D)^hsTKW3AT}cWG~Pdj(gUjZdW}aT_*;<93nYA5`+0SRAN( z{8NJa-$WGtFlBI5PxKr!0$^)|8 z9~fi=4=@_jJqQ|4Ah!G+5PT8@USDe<;d8ex=umx}K&}fQ(EwskfMfv(Jpp0`AaVe5 zngIFdR@C!TLP^6cWF|lq4jN+q-x?z9pTYmR2QWumClbB`j|Kr?n|}0uxd); z>%CnPlv=iqEt_I(W!oo4r^(+%C%4p+QHL9Gd;;sbJK?%Jl@+3=<1%hCV z(#Za1QpRWEx+;L%Ug81pPpm@#J{Efiz*oCo1GvW%Ig%x+x*-4$pkD;=y31|=K5;=D z!0o_NoibV|Rvo~T?wMBhVS9O_hU`s@>ks$@NmBTtIc-&znYsICnWt4 zod8KcWR(2_=Rq>F>?yEXe1^?W`j~R@xx`6z0n+J8AZvl5^-S?Fi^!^|GBvF6Z-6)W zS3thCHh_cG9zG~N26D&YYjwB^^d86*|Hltd;@%j3W{+$w?%x1@+Z8IoPVObP?-$iJ)%!UY|<=Q!TW{%0us}QceXA@I&|dbWboK+{j(rbf?2ESJIWzk(Vz33fE;6c1)u4gWmnlxb1te+Z#Bzm1d* zIBLzTNBs<~nv~JyT$_^7of)eOiU=t4Bw6wB-jVz|O;v~t$Kgn^(IPhv*I3M;=|zm~ zTQIs=1EZKHzF@$E>`uo$0>TRwwLaK#G+^0RzXrxO`SDWF0igGVuNSjUY^PVHw5=bZ ziU7s`!7^|b`v6}aEl{Uj^Z?=toAzU?8>R&Ve~xX?6QhyWA@_0D^3P4W<>S9g(f&3> zO4*xU+$y=Z(!dCL7d-iM)U>@)I*Np&d{Exi&iN}uFbtZY^6RU-1|4NYew{*-- z8PWr=!aq~y6aQ3Ucu-)uk}Ukd*u5J}>s?{`)dq(*KvVkHQ>}k%WB-#0+Mxf{>Q&y+ zy;sV7bYWl4$0}c7JPY2rWA!i4oehhOh=_>fOM+`*->#>nEgcL)!!I7VE=lY7B2Rw2 zj;_0Y|0l{{IE`q$JoZ`}Qt@GE_UB_x#$Qj^63v7JeBOS{J{xq3hPd(+Yk%5>7SE}; zvC?NPo4tOnN1O7FMDZz8vR6l(1uGlvG#JjzdZo(D@h;uicvMqz6^1!Umoa&tFUK^J zq(mgB+SMp^VjA(rM9xm&@{v64cKjeU_0?h93Ep)@Z}sz{y9wr+@;#B$kCJ$N5tpW)`Xl? z{-~;2{fjtVTAW&I$_b~-l?+@AdtcsOV;kYP>C}9QI@Ddo`Qfp~Pras9v*SOTIY;=b zld!@^x7Z~Eh(ahnafpcATV@SvpHrV2k<&uXjjnC4Lb*#D|zlgI$!;6=06|1C^3Mh4N56suQpbkdxU&-8A0(u zTj8Rzp4kmGOOiI}y8X-iZq`#eVI&G1p76rb708?cNSC-3r?vT#8N~!xYrpLx(Etr9Cwn_Sn{r0Yi5Xn>h`T(QJH*mBf3RFyEI=Ue&G8a_DDSTIPGf~NK zBjkjq$K_;%2#k;Ihm2>`sYp@>wb}M}Q}p&dC%SBT(9Dz$xnSi=M#` zURe7m-uEI6qS<+Nfi|ebR&!4@MWHIjerx}vjjTq}<}mVbIiNRt?6>E! zwWLdOVuuyqPd=v@%{eBjQ&gbBc3S?k$0wV|E7YnKiwh|E`eW!o?~)Ul6ex~Ji5z<- z8w$BI*>wZYoOrHWPZ!**!WkXUMo7yA$PluYFz1}v+2Up zjGmQ~pI+=tkGd*B>Q8puwTM}fW}aP(mqX=F(K_ax{#44dw)|5y0uhxv=< z`VpMQ5_i93N1@HgUwBxss?IDE9f5R~6$ zf)wj}hW3$DLZoGANinoJJfZnv`|S4eUZI7=Dam}?KI5aIBWb>h?^$UnHf?Cz&>M2; zhEBY@-!C+=6V-;cXT(nI^*G^fQ95*=?RI|nTE8vs;N+nPqGE56dF;B^uChY}Hqy~0 z{cd9*La1=FWfoGNWpjTYwz<3#nix&-WHyhHat(=mw8|X+Jk0O`(^+w8$#ylFyV+vf z$8g6Azh4+~HfA8sB=VP14q>FUJ>HKOZ0lmKoi909DywyC(u$LUKPBcnDbm(XS}{|H zTU|!EQhkw*na>j3uS8Ez3LCx~u#C@7q|{c8{1*8k|A4j###F^i8Oh^0bzMZBJ3!8t z{Hbxt&?)5iM4e&1V`Y>gNrM7#8{2@V7O%~e>Y7{_yW0;q`{!?eyUt1{wbQNgX>xsq z{Hx5A-c)<38XXDt#zJ5W$vk##@kjlQJ|oicEiZV?cCiP-c}D#~7KJqT@iwa9XA!e< z3W+C^axcd}t>y9lChPCE0F%QmQy8q;^S0{UaheP2EZqv+q^S05xS4o$5jSgevuclP zZTfPI-K1#MsGA-03APrQ2iN5q0<+P~$Kx$FL&772weQXB1hoa*OPjyB6SRc4!|!Ze zQm1ln$mjLk1(#uq^@s~6h+R3^W=@ir3Q5tItr@Vj%V~38vpw&uq(KT)cc$$*_?-N?uu09#V^fWPYhm3HkrH%{#NzSR5&$%ro+#Qy}D=Rgy6SK z>A+_B70qFhFuO7n5Qv)Wf~%x<-u*eD)7-bpC|-NFJ-9LTC9cy$($Xht zK4S6sCf6oJE}(eD**@u-L>$$ft;jP(B2+MvX^!t2tGos-RAc5A?WYi(ilkjkR0~Nb z2M-5wp#+z&-;@(*IJ;XOt+uyMWG?1PGq=$Y@nyzWmk2`PthZMmkcZbOY<9I36AwB& zZ>)Y3abgXERR?!OOsf=<#$di)Jl&CZAxMJydp(Q;r&Z{}94TDxq)8(3u>Mif^XerN zsb^%*aGlzyN>iriyGiAj?X#b?OE=#Z$nBtlZLhwLyHDl|(agbMuf1b-cec)3IsgvZeMhd#=wH3Wb{ZxLzv+dmZt@K8Vv- zS~lyfRD>`o*567?T5je#xj4&6zFjd>^I73mL=fq@gmPyXjoptoWHGz4#k;3pq%#@- zqG?xgdu@&nyh>!y#|Jas<4VrYu6o{}y{zto-9yc}zcARj_LV0-FM6^0e3k*tNvTVB zp5yx1gGD#07MXqPlI&lws`3XstBF1v+&UXTJ^f$V%m>?W5{T4U)Pk z{d|t^Y=}PzW94owggaW*+|;)T+HbGF<0mls&03INme=nf&x7WPz*iFb+)*lZmB1It zs?EIxEaTVHSB6$#gDzvnpPl9IWtea~GH&6o+XY@c?sM$>_n>g9k}nv8!TA8|7e8JH zYZeo|aHio{apw`?eRhKFKu1FESmrA`E+54eFJu&Hc~DlkS_-Cni-ONj^9iNy?r6rI z?bFyxDeAR5k9%J8XI@ZHkFWV~N|{6!X+4eAqL6y(evOm={!cC0+hTFq`Gb}tt9ol2 zvlk5>po@(H)5jkT=jO}{#b^}Sxa3oJ4{W~QcRw#SKPval$0@j{aMZ&tx5!S4BA6zc zgQ2t9V0E$GJDp*&QOJ!17xHe9mI7(75FF%_cJ=53J6eskv@u*dU$x1$T`Jtgh4NM; z$-(M!-q9I;OB{8wLbD4UzBxeiIrBRQxf#QZVk1jK2H(tr&{vi$+4BYyBp%x7dY4$A zho16PwD@Q*fSx9f@TE95<=IF^q6&>j#ITFx^IV8U;k^#c#`iz>-$hALm8J61McXYYqj0dBMuBwjYxTb+`awgxVlPw zOYR9wPo~a1DP{Is#1{;1O5#(pWe*)%HQ*7i@j}V;IXN4TMc#oYTkX4?*-bS&MpfH# z3n%%uXi58y5sg@`VBV~=sBx}Syg2Vi$G!F*Z!9vKkrK=~cTX%eEul zb(3nG)}s0P`$A&mRQe5dNz2rgAHn-cmtMT!`zV05&~-hVyQ#HQF` zZLneJ9_|LEybp1B`(mShdu@XwJLZLq=hquy zTCSQU^EGtIrVKqneOxedL2tR6Ut)ay8le+H<$2Yi{@v1EzRtXln#v{yo<7Qiod}HT z$7{u1+{){*Ybo5jwevIw@zl#+?+)^Lpwe2&JNZSH!JSnBx@Edo^ zR6hDLxnG&}ATX-gE6hmS%_Et3(B#40wW&u&yu@7`9S@Q;6%wLXpmi>Rm&w!FCo-*~ zW-+O&-lR7$_lROt&vkg8?$sjEr0z+hS>x8087k$uRq`lZjON&-o1MgIU&3cA?=Tc9 zw2SI@?%#14`Eu%3K7GO!?=;+FP6Rx>yMBm;pF51o>*LhGq(s|IZE!t5gcI+6KRY_7 zA%a-TvO>p=ywQo;HX+ZmJ-$O#mz+DF$NQr(gD>|*`c3B0Bjja-JQ+rucPvICkxffb zx&eP{?(*Tq7YDZF&QL}(u;4IhKA&o9A-ALUs8xJ_T7XGacy#Ymjl zu<=4aG5uKcgK=pI_x;q%Ikt`@m$~)kI(qLVI~1E~H+b#5_BCvF)%Os{DHIb5qctsL zDZHVw#JtIX8c=gz)96J`l+3SuUf_TUy^PPWCv{tPe__lOdNE2S3NsZVe)^V`=tOY!o6I^l_f;mr zW{Ja7eX$xj0q#CP?&Ag=H^pl&JRs+NRAl~B zW=8sw&#AOCq|cD5G=(MQg%ih^RzfS^HwZt78E6*ud&t@*JazuqLWDlCz>H}iwa$jAfT8`r174^>|Ox;MUAb8072 zU9hTGSw(xJ)9$58!ub?ucIvIy9APGUlgG=gg6ai4-3~bBGbpTN$d2U|4hU{o&~Xam z)%_A)F;%v_55FVfy&k;{<0KJvGb9Z*Tsjv^ahO|ifseDVShc9iBH?`W37>Zh7w=3z zmtD4<>|vtS{$Y1c(EU(!c09`!QG`@je~P8K26UaW#tIn zEA4d{P0TKTLCf8GMiO^R&^vlk`1}Wkj6rSO^1#kauzb){t>X(#Jkr&M(e>P?nM|K^j26!Q&ZMWy%wdZ<&$yySh3gfdE}|;gs&6% zr?c@#oMHUmu5_!Ok33^;>P^XshNs)|-CCzm>Pf&2JiaZ>*Mhk2XiIkQ&O zE5Wqj+O>?N;CB?`AI;vgrIatC0!{Xta#eTe-Z+;E1kkR~$j8jc$3d_6)8ymCGNZ^X z)gx(tY~vs-OWdw<$8k|PMq8_8RKeYrDDDS2*B&0~207*>sdYc%5_4guWx7B_zdllX zhUCGudMm)ixW##j6DA*bq3R^oBw^F-OEjiQ%$zq?lu>gk#CW_iIUDI|NS8(IX~(>T zkf?|-j~{kOL_P&@K$}I1upYh&4ED0$A=j3p7r-w@oJ%41%OLP!afBNhO z%iCAX>l;Fsh3S;}_jY>>dIdtOxk|OPe@57H@;P*mV)d09ib%V{HcrO|cXh+gNJGrY z=1<~|6Qg*9%i%{y4JHb3hPw&&;dEy{EV)g0_~yzdR-`oa$QK?nd)Tl(cB3oAgF}e@ zbE}u+kKk=1F@s}OQ;^vl7TN#9Uts)(@Ok8?bEDDQ=__K(=62$j5-70(%I8}omDMYc z@fcHl5kl-m3s;J@pJTkUCLnP>rjuiT-M~v0fmFGc=)DWK_p^d?CR3WTQDvPs-asIs ziIp01oto>9Yzr(OKlZ$Nt1$8%?oDvB&9xdXtoQaaSCi7`F zx25nEuUs+CjpAo$TVLgvVoAN2{5|ruNjXNXnMe&(6SbP|aoU3rUlps}bQ8%QML0v4 z9u>Rfj|$c1=j}Lg&kKGZ9Ss8CUehv`r0*)|9X~gBZXvA(yP=5SaF(I*7krYby?>Ft z?oFg4!xvCkRCYh9OUdostxY`j)oyjaRfArGqogQdoDFND3yHvh0E6wV{X^?Z`kiNw zo&+0-Pg)dD*JHRQ18OwIk{1RA)@CB3%TMibKIajM64lCllf=XD-O==wd#`DgUI=>|Kocva{9Wk?|>S-?M z#;veYc$N1(%&}a5KfLo;`Lu2S#wzjHVb9}YS8?JewU_w&kDfN4W}mMwmpO+wzE~4H z>BEhb`f@kNwf&)BB3zHl!bdVv^N!LMwS3&uV(g84nZ;Xc?bs#?l96c+tWfnaA<)K{_Aslwd>hQo+^d~!=lQw9Kb;+hdIZh!tJa$czFF|mb_F}cweWp~Qt4_~iG zGpdd?;aWp*J55g&k;v60P|$jA7?H^%z7bASyxn8o;bwtQ*k;p8FzMR%X@p=L`?IMC z6OpWmJ@E~iknZW=&!0_d-CYY2$Fd|V?Amw5%M=a0L2dL5{M7?F_I|4`ehFS>Rm$5kRKgPmKGu4icF#Pu=vydh z+2;J8dOK4yxy}}?mY;iAP|g|P&7E~kgL#T#B=*BzRpy;6{di1_{b)Ds8F)ROY%{;d z8?hNQu$%6OKDu(lQ6?*h)p#N<%!K!$CN*uB^IAb`ut#N)#gi)HC4|TNrDP3RDY-x_ zM?~<{Q>Pzj`MA5>r|R=*C`Z%MqVpi<_@6?hRN4cd*O_VqSAslsdDcQMQx3lm9&?a! zazsa>%8aLad<6?VD#a-dPcye4Ry?2nY!JKH`{c?aJ@lYyD9@!+cWrMt1hn_rA2xC0daKtTPQDL_2YRJTpf`$%d8SrJ&~G*8SI2ais0O^=xN|OOrV`BqT-y6f^cz)&)iS zZIjO=^telsJMC4NcSS8MzPKu%4?mlnL!Lw#sXa#>i&68bX^TcRiLhRFAQ}uLfB*CN z-Y}P)0?BD5c2@6RyM|D4x}I3*PUQ63D@V>!xm8J7qo{~vBjITCjYXW|yIE(>Swv=M zh*p^4_X+_?+x+z@_0Q7NP~yFdek)J=`s2~A#P{9X>?~S#Up-Ei^_Q()`MJiJ*82%B zFpp!s(JbrB*{O06*Y$?=%usE;o6P{(R(roWqv{a*{r(Hhxan!INOtJMRx#K8`!qb}Pn61fcd0k#N8K)z?n?sng)7!pQ zgI+<(iAvfIWygENA}nZ+%RfH!Ix!sfG3djRuu@C;dklMQVWd{bsQFD1qn>XC=5O!C zvUW-t)hQm!dvc}z#!@Uh$wW5M=QXQJyaxRlL#yIh7-`t~XVrh~F%wPeiBG-c`ILxk zd_E@bb)Is$D0d*3M&kJwZPb~ysMj@2ltIO2KT<*ll6FfsCmC=@I3@)*I>EJb44zjCby@!PGal^=dLj7aN-_TIMYF5YZPY6yCC3 zZ9kZ09k(udK(nezy0QpWQ=X<+UeFxsuyE9^CnsMkY34#;frKtqpqg|ivULm`tT-jmtxvXi*oRQ*3iU$V`V`nc8_OOP(`Z5Tc27Cw=#|sNR zQ;{5|zGEVpY;1HjH0O7wZWI`-k!}{t#69tUlUvydp}uM=*p%3JjC+WqwK_Q1Uo;&@ z4^F$NHGC$?iM@rQy2q82s}{x&a>^ zv-6Z!+IjhA2H=xWmG_!Lx-)OGH`yD*-TE>%=Z6cAGDP&h)KAR@&_FHtoJQ^9+&cCzV|I9)w#0>{h;$mQ2*!hcGwi z4{0y#*Z3B)+_MZZ2CduUj&2ZX=GC3RGv^Ado{Xce-!xvGs)2>_#J}~Fozs5tsZsIs zm&TRKMp_+sEv3X2y90ocBoloo|CtiD8!CrIuc!Ojxvaxq^ zGt|O#L!gp@{phXJ;vxp+k0}rk*qKt~j-4^k4-Rr-$Z51!W%7$*f{NQQnCcK~lf^9gSiCh9D&TXZjT8Kecz)HVbsVF(L$*#T_r7->{Qk?QCC6{${H0}l7y`De2y7i zUHA9--b=sx`MqAxAARrp)iuuZIFIH1**~A-JkCz}yIU8veGTJH(Es2ueu5)gK+BTZ zX1@QA!>0)O9D+h=T^gdem#6fia%%e5+AVlcvPC?g#h-Tk#*1YCjEmAU4$dY{Ss$7w zIXYP5-Ev%iJrJlk zd8Z*h=3CR=XB>s6{%p8n>vH*b1a9mp zU2JiOH^=IB?W4lpWqEs!Tz%vb>1X(Sn^pC&b!tRF@5||~5Pkz&9KSCn(j;uG8&Gtr zsyRZvt9;zf{L#|rwVnq#FEwf>GGm*{{7!B$!i{DtlN?J|ove6eQY%yG(DHS3WW|!9 zDi7`i3y(Kfw=JMW^lfxpDmT5*pz(d<8$%bavNQWBbpds^gwj5~)=XF7J;vA}YIIiP zeX_#w%dtJ7%Y~G`rBw^ysx6pfIy_rK?G?`w4!kj1y-q)1+@<=QCY)FPVb}dxYJ<~F z`qPEnAssFjCV{f2M2jyksNB9JSt`@-?8V@>T4~zmA>OMpeQQ&-lbLS0?OVs2^dI{? z$!9#NaU&({{Bm5>@8veeIcpufJFm$;*1*k5-@KxE-AZb^FW(pOVbEmd3VBKeN0p z)cd{1pN~qe7t1$TKz}Flo#JSv4&rVKjO-IBL>zi152S+%HZd5l%##5-Tt<}-)= z53SaVxE{Qm`Nxu?V|V$Rnx=@hqOz}hvkW6@W;GkU9*i}9YfvEaj^3(bJ}l}ANvJe9 zz49buig$m!NnT`{N|)+7$=D0Qq>!T#u32_RlD0KWc8(fvc(Pw^CV<&=CN{OCDKKAK zO;DlIoTAryz0_+NSM;)y!pzgkfs0OuMNt)Sp;;pL1RmLKx@B^`Z+rG1POGjcbdCuv z@aPE$cO&=b8mt91A z_2{5PQ%X(kV0XXMQKCRiapC>T{_EvMzI7UzD!%k7REvnsu{~1(GkkIfq$9HP zn|u7JFU#LW?XaEiY-+1CF{`n9ymGBwdGSY_r`ve$1*`F&DBZg2?N11T+#Q zyQ-%5$`?B^xgN(_f8Tr|V}s>_Ln*P3QxYq;4OXwyN*qugI=L>MduC&0|DhZ64<|*? z7~VHsWqExvH|H1Yk)Qs#CS=RP?p~pXDHk_y_h=nBAE0zDZDPH)@B9QGo9%>Se*1F6 zylvD?7BA+!+XHn!#>uNoC91ck9F$o)`QSV+Q#(eAv0%WHr)5y#t%E~MN#1$;5U+EG zKNMtSN@9{#ImVVd)@YS_<@Y~5yWt+A7DYC< zf*O2cNBUtQ+VNH?T>D1Cgi_sEz6kex52=eY`yyZdwWw%d#$meVMvd92ny0o)4dR4P z-gt3|d)#$vwVm4h(gUk_-yhQAQz>z>9phyznHe7S@m-?t#5Jhej{DfdFYCMM z@Tz5j!=|Pd7fh~L_%pB{clFjjjl`glcUuZ>| zZn$jj1&96xM90@=7Vj_q_%x`eun_YgF4&b58I>{cR+$Dct9*x**{_uTbRTOzm^d%+5rkD9*k!JaH z8z=KWJB`oe<{XYG>A73`^js|;*9#Hf3#+t4D+=OCADjql?hM}Px$S$f(e!KAx1e^8 zzPpafPUDeYoG)i4n7N+5A1=0n zM!6uUl5q04##D^Xx_PO+_bN8gT{L6YtLNczDLfmOyKBk^xc7~m@sCgVFuuYw_t1a@ zuUO~0y)E^{#Gu#7Sf+g4g)TGf+Emdx4GsS0YTS)`YYpBL7w~A`gHJ;9ugV*=Ot*6}D&B=@ zCh^^Po#j1K`qxLzo~St7${NdnNga{rvExhI-|7e4O|K_?v55Oxvoo=@VZY8H5z{+2 zyOQG;B*ykICtvCx=6<$;^IZwmxUt258MF9t{nqUVzg;%3s68Fl+_ib4dr_^@-j%r* zKWshNdw&1Hs~yKQt8RVS?mAvt{i$m7^&b@mpGo3f(MK9i6wv&*$f2dvCl>|Qtjl>* zt+R;n@L*xepVP&UU;i0C{-$5`d5c@3>h9V5f8Bg*(qZK@NC@c4EabbcyLcF_4y?$ri^3a z``51C!}8{D8e;zPx4|ZiB#JL*?N}(N{H$eawv=P1^`)m@k2X&%DH^@6*!SYbv#CPd z)7o)n=!bPy6MvbPevQvO{{R1hUSTdI{UN<;e`{n+tL(GMt`AwmoO!&t#J+CR$FukBD zRI3MmZRmktD=)&YMHdU2%1Yhd_FJ{o#}AHX_8&VrHrbT7FKDL!O-{*dzI&S`bIXCe zrm|CJgQKggo{vq&W*iNgsmk9D|Kyp&KR#10yB(_FdUx(je+@L+mN_;#-jrK1`v&IW znHlOAG?UxXp8_2Xz!VFc%1XM+2S>d+LOgu!R+ogT+~xiE?X-(nU6fUOSv*`)!j(9^ za<_Fpzb+sW1O6(snw7R^8!nQLi_xkt_X%E7|1%=YE{%^`#pwD;vxC&~&wo(U2G{nDn@%@)rVpxJN7x!S|AHge69|K~= z|0GtY>3{R&|L8UnD#gR_!UViHJ&z|bl4Hw&1UJtPI(!C>dqGN??W?%;=WN~tMcH+> z&BnxL&UD^>Zm<3(TCwrt>y`~|ms|QffGL5u2FE4~Tl$&xn;R{e5knOJnf#`*w$k>p z_R`jWeRl%A8e#O-X=+to?Z11q;-4>FI5PX~|J$#vz_(R8;0plxMq`=5gT%9A9&ZcL z_fM|;;|KF)>G$i?_huiKWG0RO@gHun8@IHMZ67puF$@Y_*Pr(PzR!BM=|919`ty9P z=L>vV$|gJE3{&o(1o~L|pFHtPmw`ZY=N_J=&hySX2j?F0O2c<-;LXUK;Vk(}Yvm}- zwd`{bgVDK%Gb1xER1997k3Gpu;?Hx~a=$RFbRkonPPCzp%)xhv%i?5v_zVxJY>n7j4$V#giN1;_=2Y?Ep=0w*``KDwv%w=Ii9d z!|{gC65yc2l@?QlVH5%Qwdev|Z96URzc5;* zE8xt-Gl{pqz?p|OemQESA@i_m{<@ZZl5SkzJma6jA%^=m=D;C_`_Ar$Lk#yx>vMfG zjDH4)816ax2ehsazXgX779Anja($C&qc>tD+qV-olDuBV%kkH#ACb}F`ZmAq8XQ9? z562L;pkoLN;n+d1gdz7C^EES;6VDRhajzwCbYT)Yx^NF1U5L&*oKZzb7fQo3S7`7| zEPCeb^JSE-#hjBXd^YP(Z`)e7725thL=l~)xF4*mK9$lW4lftGWWcM&xgUd=zWb+2 zn8)0Q+nLJ?0*6A6Ta=D#7i4rytujB2UNwHz3-1~a)EXJaz}v_A{c!1JcL`JR8#E2o z?3l8Ghx7l#e+HLxmX4Rj*!5J+1`U;s|NLjEm40ALmiEn>I(TdB*-ynZXz_2C|BDxa z4sD7)28Tr}-wjaty(81b+=}Z|&31Q8TDdJ~haPr`B5_S z(hE3fqxfT9kG7d_%gCy9>4Ayyp{~l_ex0%Dpu&7+X(}A5kpXX#Ik~+V8EU!=SM%W% z46POJL_@QJo{sBlgJwtf{dfVa>}HR_z(i)pa(GRv>dz~hO!uJKH>`KY~k`h(;h>rrNyO|v+Zqn{PsT&x#;r_}!#{cb31$Y5nt4p~E zV!jrxxaOKJJ>t*E`A3)kp=s#k{5u_``nQ+e3K=()_Dr38Q50Vg);Io{@(OM|bdzL8 z^s0%=ULwkOafB$WBVCq-b47iMiPrbxn!Z_ybc`2ZCKyVA`r1hzX zuJZ(+5Ee2dc2h&-Gz<=Co%3f5GhfV9HnlwI44nulUDm;VpKzdl< z0_g95eq7fN7;b*!*DqRrbAy=ObiUmNKbe+=`fBu-{GO1660Rb;$~NSC2=Oo=&jG_Z2IWE z)BD{nJN}z*O#b_s@u~#m9up2;%*glpHk7s}XlAOX1EQ(t0r0Ck4Sqeo0pXd~0SM1j z@0QGt_Pv7e?9fs8{9ka$i@Oy3xA|G4_Y_GLA*{@y_|^%F|&9Sif-JGOeb9LPIBym5GJUx6k5wnR~mfzQ}T_c-2tK zqebM}bjRBE5!unj?P0xQulvfpid|;7{3QYnHwF$*g_to@SJ0F9dC1Hb)~AJ*L-V^b zduv;J--hOKsTZyKCSO%OVpdc7+3$15!-)s4-Q7bQv_q3>-aeQvRW@gm)de(*ULDnR za=A9kcX_u9_Jc9p19LWWv?ZEww#JS9YVZSIM2daPFZcI=^OF zre`46XsJ59a$hpI;U^I*|lIc*VA9eqWo+o9wW_To*qW#qEzI@mqzLT`$h>OOk2178ZE@lNq$` znFr@Edih4}g98{hKZ}AcrnIvxgf(*cJu&d8u9UzqLF%;C?o%m9DpfA@ZR`MXl=!O)Ti z&BkBRS=S;zX+2u>zo#^Y0j-<$(Mf3V4nJ|FLV3(OblCe12^(q1_iV!9NlQ}NOwSf; z<(+t7kmwij>ghp`y9R5*nGJVl_Kk0FPVpEVa~dqlahy6o)&}3$eDyijH8Z#{ul8ne zfz{g%St}=D2tIGl7)-r8{|D?NreFiC0O+(W?%PVdCsmro3A;CY$9$Bf^ zq(o$7%=}DMWgL0+c`JCm7x+tFAX259U!D`~?~e|b{Qk(hN2w=yMC(a?%KLFY*@gMag=`77qLn&h#g7Pp!7wx#1{?Jld{Xn$y| z5sv?y84R;{zV5Zvl#AR;tEo>E|Iop7hoqVOl;oqMO(B;X(M9pr*}ee%sY(9NWsJcT z{XyBGYm-HVCJ_<6nZcTs(8r<5fz06G%D&9tq)NC}Vm6owGyIepJacM{88Up#0-hZV zJsg~~Z*uTLf2gWC>py>l3Vo}+8LB#y`zghM=|1UHuWi8`{t`a>AY|sPxOuoVp`K`}6v*yiQqAhB_S z!g@OeRRsxE#SMzeDr#zKLpgv?*q!|7 zhwR{M=i}mi#Kp@~0`+TS>vi;qCV_wk`ul%#>+;oy117hqKUk1*QX zZE^JS@vu3f<>F!E7*h5^WuLwKlZGFQ-De3|K`#kQ~#T~|5twe#QpzD_v}u8 z5AsXk{)X!}T)%|CFE#$nUBBV_B?Nw{@o(<>4c9Lr@Jo$epx-`w>Zu3tjnmm2@(uHSI|5(2-}_&0a`hU=FQ_@&0bx$8Gvzl6XqHU79MwH+TJp>z5GtrN+Ox>o;7#gupK~{>@##;rb;6eyQ?UC2}o}`{}c{~2?*YXE^q;}9qmvN>avNE>S zM~Er7a_A3AYF>Qe?1%Ae-Y7PZY+jMf^L%el*sW0O9V<(y_EQ29^F;*st3|orXRQ_( zl+LHgCO^K~pnK-#tIr+}e5~WE&2r!T*_9bmJ2bA5+4Rh0+xrEl9=;6hznh*=*?kz^ zPB|tz+UO@`yN;l0?q_&Lu$BX}w%tkR#;6+6fo@ayVe79E>5`S4m`Y{n`9Nsz?b{KV zg~t~Xy1W}KER1q-81*!bf?>`N^a%qOMES5f0UaWS$<2@H_tm?t*1{00yIeS-qqED%&A3`&3|lRQp44wus4$Go z#xNcn{8PudM7R02OLTDNiA-*+d?V|c_mb_+w=M9)^?STL?p9>-VCBB7>(=LG=bicE zEiUL~GE6KhK)-u<6M8YXQ}8@4NVKQv?&*S(9N!$P$KxEB+_}?e6{(k{+-c7#7&Tnb zmIF(*5hORSQ53Rqe|d)GJ~Gt{`h9gkhu@8Of2YeJj-7Cvsv` zu?Gm=%l3jcQU_rs)w|K3;`YKe=?qw#7(yq@T1=0%w$97NXdphNzKo*;TeYvzSxb*8 zi(sAHXvq}nPMT&ky(luhK^Mq&|3o?uhNaqa>#<36@R=?U+w@i)U@R}yRT7_c-lk6y z!wd+Bbh_kbuNB)FHv#-FLCG>0M&+ev89i8rk2udX!Y~|Kil;lUQ7rwsfQ`JM9S64I ziwb>UAGew3-OJdH>r03cd#APnq=wWJ0hZmZrwOF9js z2pgbviUH?dk*uiEhY3eX!$itA0`~!b=+d*)5HIT@+IEV|;QfFwyLa0SEV~_nVGRyH z2oWPwv7y?9?0#?H>hiReoLH9g2J`?gVL+!XYUFwIXu~!P6Jd?jBVgmk>Jm+=Q}BBG zTTgn{QM?@ob>Ns3OuEz#cqEp-;I_Z;(<|8+so)@*4>!TxWCOHu=K(?}NLHwxfXUml z#7@9wnI6nLr#lAFj{-U)nvk1izB&@IIDSBkYHL6kupze^XoU9&K_A3`%K$7PGAlzd z_@?!`Q2~s4#Ty7pI!tbEd~5k;^BW^gPRx2GT7p~@uEjM7ja+~rkUNFD!VTQ*ERFgl z0BrsE!@>mjMF`lVh$~2AGWaot8tw0q#{+;TocWi+gzXJzFD?TiQa2(F$&s!Q*$Oj0 zuobYE&?mU>K~E2}ZYlR=c$~snh@&x7&=`O2wryQ1S&`uE)nGBDfVliQF{Y}!VHq$L z2xZa5zy;4%{5f#dJ#8fi=6x24WR66I&!dwd+1jW%OV=DQqJ0pwC1Q1Qi1-;Y6+Wz0 zEJPzTii1AeM*HB3?&Gxn0^i#m4Y_=vx@MCH%&|}#H_C&AjEgl(qv!&V?H#+O1#bOL?AfvDjM!OJ!>@qn34~1 zYMm%>P#}4EI8I+2+Gi>JP+6DskfMwa;9b6*@$s*kX9_@PiB*WEI}B-QHb}_hh0tHS zXld6F0ay!rM$b~Y3L}~7A=*R&h{|NjWiN5NroD?fu(vNFj&fkeMR0i?Va#NRDt5JU zVnM(P#PWIP5%#Xqa2Ri4g#!`u=0fYv$J@AU24Y^if@Z#+Oi@C-?~Ak=WfMN38gyKF zHCnKUE3iF3K4BVN*^Nw19~>HBkFK(Wp`0^GkF}0Hpe;zbNLyJ;k}16Jlti%mA_%0v zBC~{%AXp+LnrcEIhJ$*bu0uja-36Qq#)Ka)QUFGar57*jLbJ|F#I1zSGRd3(^h)wGK;2S5B0;@$1;=NO`tw2pSglJ7cS9meXWcuA<5Pyekcy|i`5_a; zYMp3qYQ_3FW_6BasYQ#(bdrTf=D^i~bl}&9TEtpSgbE#Lpa+RXdCMKN@LgcNj3rQa z>mswV1cHFXRkyce*yu1~u-c6vBzlZm9a^~VW(tVwE)Z9W1`@P`u3u9OL-7P6@zoCj zux1HXvSMUb&~1N`Bv4w83bZKSOQ!5VGpt4nI<8L(zX7+NBAY}a)3fRXfSK%fAtnGE z+5~K_twFczUIl0vmT+KHn#`jfupQ;fi_H8C3rxVy+(9Zg)tW$5L6ZKI4jmcG;YALB zq*Hezjgv|u5c6sdvy|jQ#7!TOwHCH-u;?c<T;|>`zyAN}zL~}U+2CF5Co^lGur(pmW zPq8k#3fhdD3}NDE%JW%QcF?%*?L_wxQytKwaXYRCrr<)RcRitk(-+uJLJ^jeDh-3o z2-t*80Gq_c0bq5U=W$aKql;2UVaYqlKsD!0c#&JpDwUox?Hrgc>Rx;|ja%fs8Tq*4 zF@JIFRv3EBm_Rf>sR`oQm#wlgcCQyWIw$scWeDNHjdO}j$38Uwum(OdKr5A}EF;^- z*h~EiVwy!l7TOty7uit_HaCj|0#D+iXOXs{n-b_RTLQ680mw>3EEgAt<5NKn-x8#E z1jR-h8>PCMBr7;ypml~Ll;^R;(Nj8^(|ZX_Yb|mL8erlF96|lJ7yzID?fe)o#EUp* zp9KE_Noi^ZGLsEtX!`llt-^wozfTXhkKm2Y^`8tau+Hn$y<%Pg&d*S(Xr2RWjktx*l))@L@K>j>% zpGqW*=*Kf-v^;iC1^`4dW-SAc zRcq|UFiC*pASf*bHAN9>7Ysyg>^q{?C{>|dx99XI^K)70*YnJy_or%St*0IP)?@2G zf)|K?_+k*MS}UM(p?Z3J^vZ&8@Sa2j7# z*_YFy%wf7Cnmct>{%oKArr0{rmg&{e5Cs+6eeNZ@>d4NgaK-Y)Z1iL z>BjTn;$i4FXEpRI2mK1d*A77jCDeiHG4PZuG#Ew=x*k8ooaf;awhWf~iVT%a$c-+< z?)9h;+AZE}-n<*sfM*40?`JRzn7?xpc#BVf@#2Aktw0?T7$DU50S_a9<^`vni{BuKWcfEg2*KB{8fhIS#?{8bt1hg@}V(5Qe z2ww!#Jo);=;o~7po7M+2t4D<~k?k-F@~&J#(X$;5UeHg%;DV*b@)|XB7y+vIJlSf&F9WzYoklrW_iZ634pgLEIKY zR6!kr1>Cxd(@!E(unq75Vc(p1khp{IAs$;R%>t3y6QBSw2QqU@!Ni#Au~v)80~4{l zuy_I1_~jcxiK(0t;46HJecLOPe;_+arlJG$LoTHLuWqU#_^T^ z34oom5RaWXxeyzH`;FZi745q;!Z}f!lvZ_pO)jdR`kY~;DuJs2zCVi?x9DJ+&Ve?( zl*pd?S`eH#8ff!Nk?ad1y*>Es{2X5;ATU)4R4>Soev)6u=F{UH$1eoV$BwC=p&I~X zTR_~XZfug03_j~I-rXu}+zAg&!(xl$*uA+z_sD}oW*l*e1RkCY2e1P+u)$sp4YG{E z=cMXG3x}h}mZt!-jaxB2`qp3q@3@LFiU2(BbmZZ(EBN7D0q_(5Bi-*K!Sew<+_{D& zw#Pvi@yS$h)>$X7!BTyIr#mi_n@bwhpQhmWNPM7}O~I&35I`8EpcTQEh{XdQK#Lf3 zzb%Gt6$?&17_>T1m{zJc0C(&s4+Jg74t!Fn(Jt?o19$Q*#B!uZ&=pvxxG;;uv%A9p z-;bcQg~6!=%)y4*aUj>vSOO0ru_ug%Tt8l^3X?+lvcNU7T}!p;xmEhN(=pOMpsFKS z8DTO_8gxH(A@D^tm@+wXdeN{AxV&H_;w0gQ8<0NOdp_y&mbRD~I#RRb?_V}xIY-1< zel;@2bqB%E1@GXM3fS+baWE<56h6I#)87ld+B=Keb?5`B9D#|Iz+pWC^AdZw1h2hf zw6Op*Q|d7q1;)?H3kO|u6S)zk`tW!(PQL{Jw1*@`=NcU%Nsml<+xI-vGKx8K+Tyko zMtY4}f|%NaWDkanE`C1zvg6*(WDaZ}$P^udR|!4>@xk|Joer>!h&>wJ(CtNS0mYibCL&i5mz;}(GlN$j;HlEO3$usS6I zQF?ec2bTDCHwP&U79xVTS#y@P5ac$1t-KX0z*i>-&0tnf8@LojQkSFt=vg3|aqHnv z$j6W#M`nEq0$dWo>)-_)tO7=_7Jfek10J_oQY7^nnG&EiH*qmwX>ssdm!cj~gEi2= zhD^EtcLP9L{6;;T5Jsk4ibRuPP^i^&fgGJ~=x?W4BQ*fgduqH^uU3@N=cF_Q+uuTb z1r0!=g}Y9FNIAZO`7tHdq!y#znb9B=fd6?4NUop_@G9a(?&MF^>@Q}1yxyRUrMgz( zv)5E|=3P{?|#rB53xOT zWDjfZ_4MY0V25@=!*tMa6!4BO*FxBp_;5+j#cU+NUm$)?1qUoIVs1_Ife33?SLjU5 zF|o5KG=N3tUHs8t*ya$nM8HOJaS-#X%IC7#78ZyQ1gCOt`PFH+~^|fY=oX_OtuBg|AIZ+o2nC^b8fZnKJWfMJ^T2+@6=9E zk6E}D2&1PUF1k>zO9VTTWh?^$y1ggBK+=T4-5_dlM=T5^a0M1FrsDX>XzgH;DGD%a zUINh@Ne1|NRBiw#c^i#;10pa)tSEm+)u(5bBfX>~Tgi!B*e@&Ki4}8Qt_5Xv5Ns^6 zRZk&iHolfN9B?&|i{z8^Sx=TQ=dK-aMoPUSRMg*7d#5HhCi3n&*w8i*!?%WX?kKe% z);jeG8pnUml9H}~$0m0f$7lLsVA#Yx^zgcxmGWuHoY?6v*5C+3eg?S|sT6QL^E-~u zlzcyJH4PaE33FRwIX^C&be2r9-cvf=Ck@zN1?+tVZNs`?F@L~sd&6k%OD6^Rgmr1$ zeUL|BtrmP<-}UwbvZDf28N6-SpWv2TAR59VzAus#c?v;QUd-I2`DhfRVe8E1C?o?Ajgl#; z$d5!M1h*{jDvs~{+?o)~+<9#@U%qO4 zXwhY`{KP0xK4Gu}2e#9=eW16v2ar^jf_ZJ>$tK+e3ymaO7*WC3U#iO)<+?TJrEbt6 z)`_!B^Rx;~MF35$2#F|+0uGKdL<(*CC0wb7^-(8+4NNT<(S2aq8obzNn|x40Xe+;1OEhs(W^0tc0&5_*a&14SA&aT+An+ZVt3}5vJ`at+;d85K2V7VFJi4Y z+t)iaylVFOb1-h@gve~tIv8{iX7*(Ai_zTRn_+$SM@HlO1{mpJdeO^BdWtTiY)1(<)d`4zC@j96&b<~C4%^Yj?!mCE zp@hM0j>p?@ynPIrTj9>RK(riehbiO)oL9|F*$$VDyj^VY#}t7P?&D#KGjnTr#*p;{uA?Nu)0WU_bE_vwEk;Nq>u#Czd*x0G@Kr-N z{IsL*>n5!-rZEuLAI#wcwr?wUGM$I&27L9G_gKK1*-?4;DjZVkMi7=g{9EKw7wQnb z*8>}{5{!Jt^I!owfVl@a8pU-#GVa)|L?>QMj^@Nm4Tq5KAUL{mSHR~&xjD)vNfB^% zMW_%xQ*G(ljWe}F+R^HTDL0VL{D)^a z_<YpY zOAqG+!V&)hP-nZ#Cf8a9eYgUlvGrL7#o94hhFSqLC%ytp*gV^1c2(1I7gG7pl(|uo zQ;rTU_6pF-X+PApzb#QgNK%9sR%49JBw$;f&y$T(T~y(@nKTg?Iv-(GCUbxl!*s=f zaL%0Qfz~KGQw7q2kbzY>FTj@wlHx1~H%<&hW|uqAC{Mv=lY|i4lB2nkB^f%7;59=! zkHc9GwSw{XvXfyX-M`&wtoZ3ag8S2)aV3LbvcPksemls}k%76qWq_x_eOQc-3@UX$ z>P?E>8`wy2XgTW~ftcKQyKCM$#5`~uNvJ-+OQ_U&M58#b!Gldgv?ND|12@#^5Qx}j zw7943w+hD&_u)w;&`J!o3Z8haKm3mELm&PAlAYHvUL`4V0605!9P^+E1y{(hQ*x(;$Enk zTU?@$?KSXnu}z?cj0F@$lw=q>T+rZ)3VX50%M}L+q&{ck8@>=$BLS;92)a2{!j@?7 z$o61pI6f@*A6l~EodiJ(yf?9%D>++%|M@}tmJc9^e4sX9OU%2H&Vzhe>|-}1Wq5rc z>AAfm->^@ze40kt0ck?&6{HC0qI~OR89Ly>VW!=nWfv1{)khaWY7ef%R1Lu0&-ldr zjIutgG+VjBiiHbiG7v4nWsy4x%|1MSm^vSlqmJa`czg@9`%!49aL>i8LP0}L>hj|% z%fI={&qh8g``)Urya03kz*U^;04SA=%@#5zm{a?J*}eC+bi_b;7l{)@*Un;+BCccR zUd-;$sj<7UdqE|swyazs*Af42|s=ZZ?20LWhwl{)&k>?~{0L6cyMwMSD7??HwncWN=%hx?`A1utJT zxxHzIZ;rwEpfQQ9ux34%cQ8N40&ToKl((b8CG`ZP*{(zJLUUB0(?^F$e5z7Wxnxx} zsNr%x@IID}uxhu|6VpbAL&Vlq>TvO7XJJi|S$lze zRef3#FmI<=S#%Vux=Pv#cOZAaR>g>Y$6~?bt!1xR1zyBO;(GM(DDV~_UzB2d2B*F; zCOfn2tARrimg=zR)XCXKLK*U)Q7;hsuT=o&zjkW4$!el4zbvci?am*20UALCjkRmW zKt6!`IhMLFq`3Fz7&`RuWr2HWG4ueBO1>u0gN&Z>RZ-dCQwMG84L#VSCd}p^ehEee z|3O(+D?^qZkdY@Wzywz>%k?Tw&XWORrJw+Rsj8o=^( z$ur-tLcF$rM(F_5g#+Yk9xGxONYUZqxa=>S22ojiQ34OrIFK8{?)6)5>MdMTQHc_> z1l4UItjleQM5sKNLwGWF-G)`4PnAa0FzdsQAYq9U<>On;sPNrQ7&zIWeh=K{I`5Q% zjjYKF?}!fPhWQHa+umGg6@2jZ#Xyv#+~%=(!3-qJZb7M*6vm*SEdZt0`7dU50Ke5K zu)rRruSL28I*8)Q_TIWo3T=oWab4A8r1Xfe;N@LisROa*C1+n2I6Hb(2&xe$XKPIs z?_UjYa}GkGy6E=I8h0mdE0CAVG1blOPHNzyDmHuEkR~J@;w5|Pe8#i?vWRP3X&St(2QFq4*8!P7Q zkje2DCom+f0c;Hb*gp~9EiD9)KnGGQR?45+5elCK7Lyk?>ytMCL2+l^!n zQjt5*1}xfJTS2>y)AafV3oKQp|MmjXGF-NO1`oahu;! zT!YrZ2s(6WJHo(7#)eq<)AIe_6mhu=y7fA+3wBAs_Sreylr7BJ915{Og|`r}VAi|n z(&~hPQK0!j;P^(}=02D|MutK4Xh}%`HFwqF5T2?2hHXB82PVNd$B*|5fnJ8EaO%4? zS3`V zEV^jYg5Vwuc>0Vmua2<>w+)?h?7qSwbf&MnLFgk0 ziMYGJ6G!L|61=&yMFbKRWia|V~p(!+6 zg14gKS}1^w9kR3Q@v4gvweWUpl3_leIH15(d`XnAZW9|`+#S#Zad^dqKX|fLV4H;& zdeZ%L)&!W7r*u*MY#K#estGqWxvorK)8-w6LVaHVk`KWZF@X(D)M~~n+ylL4I=#tq zLS~e90I-AtJ~&>i4i3{O=bmq_NPtq+t=BH(X=B>@*trJ@jB^)oS5hFSExhW;D#*3P z70sE_+Yzh^3jiM_$v78{yOIfF1AK6L#9y*P5Eir_7IZ>DUy$HF3sVZ;bcTKupMZ>O zG(Fqih@R$0aNi6!cCmB@Y&y6MemtVeej(f2``D>mU6|n7;Sm9w2S_#UDw~Y>u(s@~ zsD1&B;#&X7b6`6|7AiHs3Y~gLI|LDk@+eOMP!V~u8*Bw_R^4ddx&@LB2j`RrE5!v< z&XHo|K*k!(b@^^BL7POB`9hZoj2vSEv19aENp1invmvW>*%LA5&6Qh&3~UF|Z~J2; zZfGaT$Wb5+JOCwrHBhJfxF7SHDBqXkY)y@O3%QH;kLjCslyG5dB`VK>G=#$vAQYfP za|blC08ZNlDJctxAL14$CN#o|ui~DgxW#C54E@Es)X)ooI26Mn^4{giHr+*Y5CN)_ z$Ij`FxzJ%5owkog`Pj&IgUOV&IS?b2_#4@=v_m-o zaqKvmg0%Rpzr1GG96KOr=MwW?+rl}W4?pJFwZ{HqzIkaRL{-9eVFy`$WP_9hN~El_ zB|^xpq>N9{E%Kd$-0u6{DgqIyb3`zxEG0r|91|`)cM2tX8+z%eU9>WpH|9)$<^k&I~^xYv_xA5WY0!bj*9W05sEpqL zk04G=G2sADHn)(?(xE}~Rq4sb7mzjxoshLkPv&^v4|UEZHfum>vigu>i0q%dXkZaN`g&CVv7~2#lf?az z?Q5j+BkD_4<_PGxbAYYOpnrFP22ciZJfg7tTazy35~Ku;ZrA&FR)P}-tKm^ zz!eBuH;i{{&sMSZ2le9{2x?Z{T*>j^zJQ&P5Ts4k(z8u<=r17Nt}R6QhtEh-PR}uP z9LiMy3ays}u883a_FIO2@V_nV4U&9mQw&{usg2EHsAHT#^0dm9s77Eol4%~vEUi8- zd0TXN7ux&;PR{;5$vAa(SJeje7Z6zH3fWzw)Z>Ccvs^%r10A5mrYuoWG0mXJ!FHQJ zPqT{@Fgk4N^w^9S@idGMw_QHx(uJAKkwHes{WO#(Rz=|W;sxv^gX)pPF>e==24EF8 zAzO<(^=-|u^i_}%*}VVI%BuA78e5k3FwQ_cx}ieH7uj;X=nn^I%_2~BgmOFS%;(k+ zK}dhgRqANXDeLlFFb^8jeXA1o8aeYSCM*@Swa}x5L)}?&8K7RrO4;M>9-I?sfzY;L zdvtgsiXai#$b2HUNYLB#_Vi(oe34|+fb_HsYe>|+ioid=?`S9C= zf}OymhQP*9z`gboN;nV+eKL?50PARt=pG`1Z$TzVm(g6olg$sxgq5q9B@PRBiZduX z`XtB{ITWpgAYHO8di^DV7HlbD0Byt(qi4r45IaM8iwL$vd1j^wiksIm7|oDeiHyvu zgUoV6A4EJ}utV|&m?o84ui2eu4>=SINFkN0p&qo7LAmv$0EddhK)vrNz0x-gMAnyBtZ^I^iJkoeOWeJ8Ep=KDEn>A0_#t{M$st+3z4@t=}DBE@3WKej&fN#gikK7h9#R3OfGnDO6JzmKV z;zFpWtY)KSL8d2dV=H44BtevpEQeGPm|Lihv*ChFjq=~cOwJ`1|5OsK4YiQv6^AGY z0Sjpwa-Vg-d^1tsh1hjOJtxZby`&a^qk9qIl?TfhoC6d$D^Si z?V%t!CJyuj9$)|MER=%cMfns`(AI*{c^hOaRWArkaT17eU;$2}h!2(BJlR^(9dbM7 zK-&gzxUAVh3WqT_TUYQ)F|Q2_F9ptr>J^?XE2>n1pfDJt4F&8h?;`bJ6@D1X zSr;HCjomwhQKRVDkb@C|D6b00vmRW|r*mE($E`Okw_;ng!47&1DxYf;LM{X@C_fsR z-7s(wT8AG1=L2OSEE&z@sKwdr5-N2++=^VtTGw2VE* zp}2u8^2408jkiuM zJ-PLQedo77$s&P`XY1U|&vk9FbLhaHrE#rZFGaduoTxx;^WCG&$3t(uvP^2wo-r-aVt(#J=|7j;Jvz!JfZX|5H&s_Mg7d6 zc6;oHo-=!mZ2Z#kepg|eA4*9;1#V@ARNZT8;Jw!m@p-a#>9wfRy@%sYwKS4nao%MW2DF^j=6&S zQUJ!KURV@FuvCQqb$OOHbWYz&d7FPZ<1bf3$VEH z0$3yOJ~2BBKA=nR*k3Fbe-B*nP>4Xp)`MxkWqeezAYMS7a;OO6OU<#b;k5ugk)FL6 z7~C05A1VA!RCY{6^P|4EhUDXApT_w}kTR!#52me12f*8qh?yEhd#ErB2?O~u9kSSm z{$tT?pb)8xK$n@`iV|(1pcw~TE-VWXuqyfH(kFJPWku{M2#m2qh&7Cd6${QHWE5gj zN`?=o*baOSP-)$v$w{3>xdY>uihl1{w=+OkrUdQObRk2i5SgvL6VfK)rtZ;)pfI%QStDWoB& zxh$Go5>?-eR_J`-o#I#~Di8rb2v^)s|hF98o^vb{A9B z4K*D^{0UuAP|A!%AYKTL)>LLJGHaTT&t8h}a1Z9kO7f>$4^J0g6%CR#++b4$6fG}i zH9Z;~>`D)osIdHO53R=DrWwJTeM4aANyu$pd%+_**cQ!bhvbN85z-j|PXknIQ+4)N zHdmbjF{EBlipehOe(5a*FElx`lta$xY%63Eu+2}2mF=L+ZlO>;JSQr9F%n$rReOLQ z*!9H)E-V#NGzu{%kw&l32&CIN9L9mR`D}XL7F(N^jOG4CsMHby z35qMM7j3yVL;nujkVsaDJ>-r>2SdFxb>5s-NIktce-*yJ`{ftErK=WVK1ju0yP-Ii z2=kF+T*(Z1z1Wo_V@7^|36h-c|gqh`~Q@0icnJ-xjH6Y3?0Z- zj4=r%EwQt%YQ_qsHS+6XI+C1=}grg4Sb<4WWRIYOk!@AZ5pYkz;O z?em%U@w}h+bH1MEbA&6Y%^i4L-hII5U?Wv)5*-MjQ|_iu?iu}T9uKq}b1fXxV5In- zxJzRo-ggZ8>Vf0Uo8zE+y)NMNeh_RMC1=Oymg#5PeKE&tSNWFGIMi&nFCNgc`9c+Fc zk+h(G!<@<(Fm-d`lR-=fqE)gnN4iMuyGmRypPyn;l5ZT6iEFiRlJY*J`H=1xjb!^$ z)fHiJZ~V3<7$4qy#J1^=G%q*oW(bDd@UsZVz2>cZn0vCG5^|JN#iLbe#&pUS`wefx1gJ5+^Kd?UMHke=U=?WmK)$ z2X4Ac-3$+6ym$nztsy%#@98@A;uJll5CH{ZDt3-m)|XypuFop49TKNVKV~mI=<#kS zqmp8bm>TGW#(4uD8L5W$J)=(%Mv1iG64u0Qm2K@cIRnJANw0JDp zZF_Q|fj9`HG2N}CJMH4$kI`xCaD#M~5c zEhlVcn4CW60uY(`ZSoif6Pk6+y)bHixNUu0pN_*>Bt9Cl-X z#nDNzP^PlK2d1k7cJ%%_;ze)G=??3%ppn)`;jZN>)=mHO)yw!x~mJd)9YjvTuyw`0yeO$G`en zGZVtao0AC;8~v{5wYp0+N%tDU2R(OTvj6GCixNGu?8@=7YrmqaU1_lY^cBzD>i?(e ziWMBLx?+|{n%6P|ScT4VWK8NJYdU%vxgiq6v`-i>_oG~l8p5vH)nBg&5=H@ zaEsLz_tKFwy3hr)D7qg64N4_p8XInU;T~oFu;Jga09tX)P1bP9T$O_%bp|1UR21Xu zKY?BT{o_q_$FEyE961g$J^jz~+td5mM2rK^$ArH&>-v+xuwo>bm<-!L3tov%8+M<@ zU0cXW0pTDzM#8Z3L_Umh0f2MzR(le{CguTp#Y)3PJk^PAVXiz&)ljyQ+Rw!C0SD z+jcC){fU9Gt7RK_rQ0E`#e!WNpWiF~<$u}q&VLq6%MfusULUgSeO>hsk3Y6Cf~CPu zwa3oIO^*8aLJK_FO}gd7dxVr9kGDLjDW&20f=dW{3p!6mtAdm@>^6T!)JD?|EU*t5 zQpD$cYZ{v_VDi~>dY6t2wUoFP=*|suW85ejvuTmCa1UPH{`(COakL`$ORLEixW|Xc z=5T&ST#-_CulQ5ci#8RjZ{moYe;mO?H%Hn`#PX&9cyEGhD8k`%G)O|pG^HZ9MZ|b! zz%tpSIF^%ehL4$kKaGigY2mLY1CJ}|R-Ol7t?ZmVp9=%E=14H+NilF&7f#dOqlVqA z9lwY@gb(ANfp%BF-xyayq~K*vOX6I^_nQJ!1ISrSm=MOXpw!0*12uk|r z4Flg47n04gTT#f_w1ySm68w0^oWc`6iu1f$BnDM;S<~f-?8S(GZ`Gmki`hklx-XTd z*Npy`=>%LmMd4=aadTkJglyu2OEQE}+OM{WelQ-guRKxcN0X)bVuInU>)&LG4QUp%9Xc@7G67bT+@AOxAX6g(QMAt1q8xlmSpj9`Hq$?VhzvzyLq|y3b zJxfR89WvnZ=izXFL9!$k&p?Is?tSRlxPGbXk})k?^6bxvjYhEf*;-{GM_Bty!ANZ` zVjAI8!8_Y3Vk?q=F}w9q4!G(m6qr+8YY}xMCA#R?JJ#XbipQSQ<-Z0OE;6<=orx{l3iGHdVJ) zo%GL3kM2;$Z+modlHiRRdyWrs3E_RG=PvR=lKL!Ix-f!+ye8*RFZ6eDBDP(m(G3DRG z&Gpwl(N4%d2EQ`tQ*oyh&#t84DmH-J-hMr>@R}jtlEIVlaF;! z!rZYK(nA;}bvgLBD2G%^O+VdoXVhwOol3JtDDy&>mtQTM?yomfd`1U zXn`u0(@o!Vq{-W#&!b;YL5yYq%A94LabhN%h?ht``dxWRtWh-Aw@al`asT>|noGSr zlC~!p3#>seW40Sc^7;FzieAeA-w{T=)FE1;P#4#+{w@;<9Db%3rzjY4j=e*o7qw}+ z3;2<>WC-D$+<+e$LA-4lu^-&LX%HJlSmdCMC66iuJUbE`1eoLCW07=&ZPR|hLc&MhY7r_my*NcXkl_vC)modW=S)rCV84+G z0pixdh*R_c4}c$-V5V*pwA{0R&<9JgqYjW01;C*s!r)AMY0rnEiT*@hZsq{a$9BRf z9&QaHk47yP(2%`xnQKZKUJg^hbV+@6vi#{eK(jC=D2U*uN#qrlSIV>#&Yp#qL!8hF z2rFR}fsJUG$quFO`r)sgIkByUQLSBui4Ts{E=P$k>1@?-9{(Bu0zs#`q8*A{9|T+V zf$rSQKW(Sv8%%tMO~0<0(u-nEZnwoy}2jq1j_I@-; zQCN;Nu+?pf73>YzI7)F*iS7SW1U9}+nO9NLpgJ4GZH&7k*ELU>?ucNE^?uzPbNx*}6LA4Zu- zJBy4Ggixwb+Hj|b?&@yV->unhcGtnNh-o3s-!-!og@;Ee^Ln$kfAaj)!uHk@b5d!A zub^k^DKHB=^lqx+R|5h?oN0E~*s;ig1~|C{=ALuCnQWs-O!{N7Rm2pYmW#x-D#Rcx z8j&Q2E1i8ML!|#`rZ(B4)q}m`^}SeE*C7tO3FPtj0-C*%Vo3xJ%(0x3e{M&xLfv`k z=hE3Qmw;L{Ntju9;^S${h;te;W3D~eHqNir*;`K^zs=xM%HD9>K9s`fbmA>X0pvF5 zs6)2qzU$2HavX~mVzKCbk-X0!#NXmh!~>b+=vYK)FM855weT@AF)kwgx;E-f*;;)Z zh$EyjJ1q#hf!1PTI%AUy*bl~#|CNo((l+jeM+TOgf&Of%iFF!3JS>WdvY8w zl#s&j28wC<80AcE$=_t|@Pp;ZQ5;~I0oP*`TDEj@?bIC7%`0n8_inxOwreCJy;iPo z+rr&zd8OmwmhW>doh48lAjA6~{LGBUKLX6>m?p>4StAq&^sT&Nht=6I*@$_e3Mmf- zKcO>+5uPr|Ouflkdh&f^U;t=&9cOc>pLkl+7<6a}Ql|9W#>U#-CJLi0f|g%Bz~0b4 zXGYgDXNj1w;8D4`NW!;4znYDfcdgk|Pdaww&<0eD7;G!6;ZQAZ>&>=$ITi&Fr^X_l zFy1QSc`q+6);=p(@roR{l|ZvtA&iF%HAkd3HdFic`#k#iZzPd{p&aa7TAKRujmMd9 zU&G*9wTk!U6amXHE*;Tsj+&L*cM9ZIUsctK^eZu<58N>} z7y!090oo(v*Q>t6*)n(nNzb4_^xID~so#7;A!`apl8E!B3)fE;!Vxj&ONYDiqCCvh<+aJSCwIiRWpH~^wsTO03Av z;qKza?TM1ZL{2oMag`CIptbZNWWxv{-<c2BQI`Q@oQ0?(Y5PG_J9u&D7Is25-d~v)AQH>PTl(9z zU*P|*oNns_F^eCs?V-#1=hJ1Oh+q_XCfKAanO1;RCX;uG7BgS8`Y-p3xx%X!p|JD{JMv;K61|$(Dhx zF3%bewKtgI%&X5&5e7plK$D7lH&S#eH^H=Gs8<$hgM2qx2F{+F0k`iJj{mtq1PkJj zWJk*=7ssq%gPco|i^Me^_=Tb{$X}7Sm?RA@pAmzS zKa6mIBe<+So*Lwfl6KpzI@S~y28+Zf++R-u_hCWz`3LR=`5Dhe{oZVj^yIU$tFNsi za<*CqLN+ofP0fZFV2C;J7g@G}XHW=um&Z>7=9%!E525qLD6<3`=Q%ZT)I)LZd3HkN zISe?7zc9E}N5e6M$<914C2#QUU2$<;-9n|A3X`T;*fyDmK2cXRAR5$ja|Qq($b~ zUQmj}lL4NW!vsR_LsFQ7rYaTao)e5<0{lSms$iHxeR1Hx$z%L>&g1cOEhX+c#3dnb9lh2kn z7Xl9atpQTI*tvB5M$5oDf{=KwN#jJrotl{mxE@OW#yIFct}_cdkr08Fy7S0%<$mpC?k^E{)T224A)UNDM0Q#M99C9leB&sg<3#in<&y=q$3tNYsXUv zzjWF<1uj_CPq=^wv98cyW$#O`ADf(VoPyaCkyvm=jp6%-n+A!na4tnY6mX@C{Ty`M z5%N4UA-}j{mU0;+_U;GHjPHvrwt*eUjSVh1Vw5mkd2zd0pqE^nAcZ^B0R`+E1C`G1 z9BxftFP|wqK6*Djhe;_bO$ElY)EIW6RfPLG%fL6JM1Ve>fHeP`ueZaRBa-5gknBu2 zNJZXeQckYAt9i5zm=OP(1cyapH2JwY1)`5NMn)_1mSAJKiUu;E2XUkuFg#%V2mfP= zJimjj!B$lTBbU&-zJsh=eQ%u|*>09-L<5CZ*%j#wG8hW%yUjlk^@rW?X(F-guX=Y~ z<=AiEev;?474j*bS_jFn1-(Lo4vzEk*@=`Q5Tg-f!GQYaC~2JKswb#GVkVPhaF-JN@J?OLF;Ls6@lz^b2SKb6SeLOg|;Z(5~<2l4DE5{&h8E0#0 zS2Uq;McfzOnIOUQwP$g<;ux$-G6$V`uLJbO13!Bi z2~4&6J+LKF=^NoBjhs-M-tT=C>s;E;1qi6j3s(+ z4RqFVqbMSQGgbFotLE{2(XK1)?r1-BQ0$@OqP^Qez*RL8fOGLI#nRlI(#WxfP+=s% zIj|vAeU6vuJI5*t7q&({Cmk+eISfY3U9?t>P{@FbTkpeAxzs}|9D!@qGiH$QN>|)W z!F@cR4a<0pkl&BoS>c1h_a@&S?YD9boj3qwA;rd$+Bt0Q-PuyIlZB|jaeOKzI0R<_ zEL_0=Z^~|$k5Z&?ahjo{lo#P=knJlt>aCj##JE2g2tG&7>4oF$FbLtK+HNT?B_HH*0!ik5wOLr`L+VE}OtK1Qn&D5Ec1p~mP zH7-v1t|zuHyKuYkP|0f8QYR>nhTxU;bm~+wkL=RL?~Z|b-5h1(I}h}q|Ar0 zGj+5dZvYPn2TQ4=hma2xtcl8-b?LvZ%msldYxmhw;>ae^I8u8Cm3C zk-khHFdtGIO|*(%E&M6K9WzdmvXXs8BDx12#ihKu8hs~%i(AR2Cq_{$U3gS3b|WW! zW}h?XW=2f}yO=n%8iza+a!cz_AWtU{)bi&EhEXAeg)js{cduplmz=|f`|n`euxOZ` z6JnIz*>EmbcKGR98YptJr8%rM4D_2J}19^awqPIzklX2mR#r(V)ed#{Ivs ziIZ3q3zH3X-i8D5!FX3hvMYX{wJw8t7Iulj$#S-jGZpy7x|VRt*MXXnCQkGc#D!4t z5d=v|!G%zllk76}22>eAZmzwLG|taFW5a~xkf%FjZ@@M-nuTDkn7Iny5RPytr47A* z(dt33v<9|}B zu-GAO9JvXk@Vuw#^rV_IVSRSwNOlGg!4^{hKVhpqtaysCM!lCZ8f*9=IeFF9Xm~4I z2Tnz%8*zE2BJ<)odmkV4kUZG`>vE%TSXrTG{rMlL)m6{G{63G+>2S3O+?^*E*B}Wa zJKvAXu8Y7#p%-?wQ9bkBjh?Ce581}JRC1)q;H;~<_mS*;hL(9DImbPuK*MxxS)-qW zmSRUoat7&wd4C?XG;%QEU?&O_9_?oEgfoOWA(w`8Sz%)LD2LebSHfsmhrI=$&A}0G z>-_m|En(k;&U%A=p094E+f2W4?4;I6(Md?fgSXIJHM17}*Idz;yD=M>8ZZ114a>HZ zeC2(T(xWk=T7PZXVLYT!%XhVrxxULr+qp&1w#}DUe7^iQ+)f6t^oqrOB*}egU@koA zGY^)%Da(UqLmM4IiK0-JCa#J?)p~x}yTxY$hb2~5H@wIjH({KSYFH%zQD#_FJEBAy z0*MW;6FpXgw3CZkAY|`AkG^o2^C{T!<;vyeldRQmB17^{Gs1TZ`pFSnr2zv~{cJ%} zw+ojC1u@9&AnYLId-lb7Og$Uo0VSWaUM&~}iJUr4_BWZdfV;c}DR)We0Y553S1VEMU$XU;U6SVvwWS4|WJ zg~FsYzO6joupw;f81%6E?*Jr{dVKlx$Ky)U}qXn*tgxhs$F*{c(}v9P<} z(fl-N>WUSEP$v9N-2yQ@U(G|~8qi~I-P8R$(C4&jBiuc7x1NlQ2+*Be8T>jZ8^QjT zO??1_h>woqlMybV=c>)H!=cqH=vvbrieVL|qrJ~1{QEjsss`*8iMA;*bo;mq{%#yi z=o-}RMG`I>jgtn`NNGURKwn=+L!Qa#cR9M{$N7M19UiGLS>Hmj5E|^p)w(*g6c$|m zafu4^yV~>5Kq{mEjOW%LNU;}BdE8Od1mXy~gM>hT)u2bi3ivTA~mpGmF^ zEuFd|UBX@`(O&Czt!R#`yAikpUEegFfTFj;n@AtYy6}g^bpw-0kty(iBS<3xZK49w zT`)-`c0enMvsBfI$|eB%3I&VsJMuodV205chbtF5Q0BWo4tYJO8-=bnge0Gf{l|>C zvT8>;!*n`8-w2SKP(z-1D^iDWqS5_l(V>{6Bs8be1W^Xi&?rKzeemUVV4Z8p zF&oN|?qRM!4c<1jbREEQ@tJ92)j+x(|G;!k+fh;%%^&v6)0ol+)K1e402D!cu+3gf z0ScYnf__ruN6~(qu%ARBnkcob-yF@Hcvx>;%X-VP9u$`vlb=Xy^t1-Ldo6n8Q0xr@ zXhK2aff#`No-Vj2*YwRhj+n??v2PwIy@bYjJM62)x3CtbJ&qY9c(RFo0-glbNDRF3BSa)`M3Ua4=CAxrvB4rGz=IuCk_Y%01gUq-vu{_<6CW33oe7=9pz$Gf`Q`6 zh}0#|#ST1(!vu)W0|mDaR4yi`r>4ndqwwjEr)ZiHp&m%9juk&oN{x7|CX8WQ5^jAfXR+WQ?k6W&+Ng{eMU{UxC6To0tjj zt46?=b&`vzrimSJa*;j$BJFM1U&yu}`+Hjduy}D@?x*QWO7l#RCA1)zQ93AC2eDTE z54M^29gMy1$+K>=7^DP^E#|qf~;qXi~#|ZyaA)9^a9d!su)5^E`OK4imD}{jm_b) zOeXi@#DM9a7eu20@bYnT6EWKPR3LvSyITAi2x`5oAN)1ZUBxCxMhk+4iEP;(}`3S=QXY=JrGNsu-iPzs_gOmFnx z9E{`@RuT5KM|1tA66BU5QS(#*rQ<-}!PT!XFRZYCFa>>O+o=X&hzl4(L^FT-c*_E{ zVrX|>VT6|*4bz19!iRFg>Lj==c~p*w->kuJ+8K^9rQl*5Ik@Gu(?yUM{}bED);SNN{xFw zzVbh&Gb#TuaqnN6w#eNt*i0tKKn)gU>wEtzci(~CW`Nz!QW4=fgoIb(Js4yTi?MEm z3>?XirhSmfJq9@pE36WoFsLgNvk~$7V34mf+61DJk#Puotid>{8FQaEG|(qPX0cW% zTgRvIMVjVO*q2A+zrg4KEP(u@OfU2lHz93%?ngC2w-mvu<+v4(FH$2_l0Ju!=t+BX z>XQWCIt-OO!u`W8W!}c1qiNfx-q=ww1Y*PIuJ$JZDFS8DFUts_9(F}s+&~-jOeJTC zwwRp;CzQH8S{b$&;estCV@oJ4Vz;!H2B5Z(q`0xA?PK6zgLZI%zt05Z>&feV9pD}b z9^#i{h?i;!6u|l}Dt+c2x{m*e% z?7MNQ^;jG(iP~Z3i>=ey11A*N& zVQ2zaF$H_2My{DT{}EYrCB*|2E502C$l565gf&VXsEA|^dSh3a(=zta(T6=q^r>$b z4|@u&v^3SUil?U&pAo*umQ~W4RU$EiR0&$p5`i5{*Mn_kO>$t*U@JMqq3)dAesmwz zxgsLzt;%cQn8I-7IiEb~X!Im9=br?gQA_{Ph8l zD3t0j37>^&$i$62oot5nMEuMsu$Zg*(Xg>WOjd`hvLTfM+nb2AS3bCFBf(vKwja#vJ zXhBJdA@5?M^1^M5yg}aj5Z=wx&^6<55O7CiBf%Vpp$n_#7$&Pg|2tVQxCgoUz}3`E z@$f%=B}O$HXX}g?A~ro@D`mU@A${?fhZo_jIPf0PfnRQZX8}$%1*aZ{1EhkG5|k`M zh?i2k)i|Acn}Hy1#bAt&9yzk*RuSi@*PAOifehb9oCfV4Nd}`);EWnmP8Kr3ObiA? z1slqMczlGCjcV{o5djwi3`RD-?9(tYH{x6>#<2pqZ-d+~0HC%Z*NJdRFWj+a69@G8 zlKqK2AuY_#iQ9>eol>Lo$RXqaLyKBs0XZOa1vFBMucRu9700rX+nj)9mjCd;!Q_IQ znEcR}w6Ljogv%qC1ouhy=TdYp9yC9xkYBnUX(m}M?jZGTJ^SF_(|3Rc;{Z#X@y9aX zfzKf**ze=mz>YkdVHP+N{B+0#UDIt~biHxuNb=r}39%5MweV*%`8GYbt*nzleSS=Ry^UA0XD{y~q{QwEfFqUNijv z$2a@X4&gTx(g&kk{4B^DSx*SH$d;w8MJqztKKjGWHJ>h(?SQ^`(V5hL$Uvxy&@yzf zT|D$uT%W(+SElWqEr4VanBZ{tLG1Bgqnib*AaKGHp#bt>LU|73WGS&a1jkKOlO=pj zEmjAG5JLA)P>=hkiD~}$Hs{;BlID}0;+n5LSn+&=Y4gXQUA_&v;K1v1RGl^*3_*4* zYv|6tgE;ZIpRErgqfs>U$=2zq8#IYo2j$|au-9cUYMBp6ZNC-e`s>Fl%FfZu=#HxT z8N~Ilj4+=V!Ec@@nu@}fe0+z2HGn^%rS{y_;xC(k2e|gg1|Ooq`eI_GF3? zMf3ZF96zHPa`_KChS?irtWDT9HO1(|x!q`@S3#ZpVGSqS-AST2E>iSNO;`M_lKzI{ zWvsyN4lpG<;ZS69kPDf@WYQ1}N@ikT82(r2iSr*JGIFW5(xS-Bn zVIqqIf-afX*$zv43)71lL^7G~8B2^bIlfgUGhQDou%AyORV{(MsS4^NO;&d2J@SFL zU@F1b8WR}VBi1O6J#T#I4ve4Sk16=06Zk^li0vOkMs|h(YjvX!vfv9g$%wcxveG|w z(Lu#V%-YyH^NKj?48;}Y zmhcN@Z#W;lXu>`)_cgE$!$;DDun(AQSZd`oVLt9QV~ulZL)6~{CJl$n;`tlAUq;ZK z-bAmMq^rwrS%d*=#)QLWz*o7HCIyh;?B|DMIlxJ!&}!R)K7|ZQl<@fkCIqfB!-WZE z-!PG;xiz=j8GH>&<-X_stgf6Ug&66$$JIMut~m+Yk;%?T6EouM3*stxEBHDI<{bsk z3Kd@XCKJ{~9r?GQV=LC)<)HFWqLyStnBBtjfZ|*FpD`p|u6*I6lOZl{jp)?7Cvz{25nX^+%!UTk3RTCeZHAUSi7AD ziL_&HS&So+gb+!(0Alb9%`pp+2Ehn4cR*%~(L~YvIFN%xlI$A>-jV$;_~b<&-baoa z8Lja{Mrw&inZ@)qJ&KQU>b`t?V@2O~0v}d*qBYR67UGAX={(Z~c~-c{=)H@_GTXpk zX;v;S1_Kb)Re%Q=Sr5!wwL&ZIvXEFUb0tpV3!pS*uTL&-pqf5pB=Gt-o^JfkrULuF z(dmQ{oTT4Ol<+$e>yV^mDtqY0lPHn#p_eo#VSSP)VL$QKkZBv3Y)2C>f&s+%Jw})hi%<*Xkis~?) zXeVPrd}K4nAU*{%e~>@rL^r2LooJI*$1$7BGw-a<-A+4WAQNzh>=%VPs3uXqj6T1@ z6oA>8zc|ty^dw;@Uc-a~2)F}o9uv|Y2a}y^d!ZxIghDk`>vX&#m);ypZ+c1ivroKf ze3(IZI~*xIH2(I=vbY!b^=*GTk!(b?7=EzwNqgGG4WbjwtmV+BsO-bwqtNFD)(Ru2 z$7l}jUPVhwP_D-u4ma$HiT*)C{-=lB+lcNw=V+pP$j<4z;Q!uVTDH_$Sw z+JUekzFt1~*d{%L(t^Dr-EizpV+4Jo320ZG@;*mhpBI0>Tyv1vRp3rb? zEbT*JZ!nSTZJYK%2E}}Ri^fa%5+w9Fv&C80jMxan^d)ZDovNlT)r_W=_OmpdQY4TI zg$7|M<8O=II!{#VB*~3GKfZaL;sE{dJrV!y@9GPBspuZcfSE%2LSdC@x>hgs{nqUt zDXHd)w)8jGO@;xjun}hZPjiqbH)7~>3QSe|O=av{R#*m8`O{CzV)j{gde*8V>VFID zKeCj*fR)Mm+H{k?fgGX&Ex0b&$G{26l>Wmv&v432CpR}F_`!4R*g}{bv=49M)~Sxw zwoUlP6vKS}CA~U3p%o)g|G3J|yhq>pUbceWSewq%3`5aZ%&IncQ&OIRI>VgVf>O$) zF#;o6Q)&;xBb!p`G@R9x8l!`dW_LT6Sm1b&=VCNk?lGm`sU&*_eaFEeo~t@bYYlf{ zOT$UXJS7d2Q*KE)FUAcXI=+_Gh7zp?LBh=P8O~BnwGc= 24 + ? buf.readUInt32BE(16) + : 0; + const h = + logoPath.endsWith(".png") && buf.length >= 24 + ? buf.readUInt32BE(20) + : 0; + logo = { buffer: buf, width: w, height: h }; + } catch { + logo = null; + } + } + cached = { + name: envOr("COMPANY_NAME", "Jorge Cuadros & Asociados"), + addressLine1: envOr( + "COMPANY_ADDRESS_LINE1", + "Av. Revolución 1234, Int. 5", + ), + addressLine2: envOr("COMPANY_ADDRESS_LINE2", ""), + cityState: envOr( + "COMPANY_CITY_STATE", + "Tijuana, Baja California 22000, México", + ), + phone: envOr("COMPANY_PHONE", "(664) 000-0000"), + email: envOr("COMPANY_EMAIL", "contacto@jorgecuadros.local"), + taxId: envOr("COMPANY_TAX_ID", ""), + website: envOr("COMPANY_WEBSITE", "jorgecuadros.local"), + logoPath, + logo, + }; + return cached; +} diff --git a/apps/api/src/reports/outputs.ts b/apps/api/src/reports/outputs.ts new file mode 100644 index 0000000..7c6dad9 --- /dev/null +++ b/apps/api/src/reports/outputs.ts @@ -0,0 +1,433 @@ +/** + * Output renderers for the reports module. + * + * Every report's `run` returns `{ columns, rows, totals?, subtitle? }`. + * CSV/XLSX/PDF all derive from the same shape so adding a report = one + * registry entry, no per-format template. + * + * PDF uses pdfkit. The statement format (edo-cuenta-datos) uses a + * different layout than the tabular one — handled inline. + */ + +// pdfkit exports its constructor via `module.exports = PDFDocument`, so a +// namespace import gets the type, and `import = require()` gets the value. +import PDFDocument = require("pdfkit"); +import * as ExcelJS from "exceljs"; +import type { ColumnDef, ReportResult } from "./reports.types"; +import { getCompanyInfo } from "./company"; + +type Doc = PDFKit.PDFDocument; + +/* ----------------------------------------------------------------- CSV */ + +function csvCell(v: unknown): string { + if (v === null || v === undefined) return ""; + const s = String(v); + if (s.includes(",") || s.includes('"') || s.includes("\n") || s.includes("\r")) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +export function renderCsv(columns: ColumnDef[], result: ReportResult): string { + const headers = columns.map((c) => csvCell(c.label)).join(","); + const lines = result.rows.map((r) => + columns + .map((c) => { + const v = r[c.key]; + if (typeof v === "number") return v; + return csvCell(v); + }) + .join(","), + ); + const totals: string[] = []; + if (result.totals) { + for (const [k, v] of Object.entries(result.totals)) { + totals.push(csvCell(k), csvCell(v)); + } + } + return [headers, ...lines, ...(totals.length ? [totals.join(",")] : [])].join( + "\n", + ); +} + +/* ----------------------------------------------------------------- XLSX */ + +export async function renderXlsx( + columns: ColumnDef[], + result: ReportResult, +): Promise { + const wb = new ExcelJS.Workbook(); + wb.creator = "Jorge Cuadros & Asociados"; + const ws = wb.addWorksheet("Reporte", { + views: [{ state: "frozen", ySplit: 1 }], + }); + ws.columns = columns.map((c) => ({ + header: c.label, + key: c.key, + width: Math.max(10, Math.min(40, (c.label.length + 2) * 1.2)), + })); + ws.getRow(1).font = { bold: true }; + ws.getRow(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFE2EDE9" }, // brand-tint + }; + for (const row of result.rows) { + ws.addRow(row); + } + // Number formatting for money columns. + for (const col of columns) { + if (col.type === "money" || col.type === "number") { + ws.getColumn(col.key).numFmt = + col.type === "money" ? "#,##0.00" : "#,##0"; + ws.getColumn(col.key).alignment = { horizontal: "right" }; + } + } + if (result.totals) { + const last = ws.addRow({}); + let i = 1; + for (const [k, v] of Object.entries(result.totals)) { + const cell = ws.getCell(last.number, i); + cell.value = `${k}: ${v}`; + cell.font = { bold: true }; + i++; + } + } + const buf = await wb.xlsx.writeBuffer(); + return Buffer.from(buf); +} + +/* ----------------------------------------------------------------- PDF */ + +const BRAND = "#0c322d"; +const ACCENT = "#bf5a34"; +const MUTED = "#756c5c"; +const LINE = "#e4dccb"; + +function fmtMoney(v: unknown): string { + if (v === null || v === undefined || v === "") return ""; + const n = Number(v); + if (!Number.isFinite(n)) return String(v); + return n.toLocaleString("es-MX", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function pdfRow( + doc: Doc, + y: number, + cols: Array<{ label: string; width: number; align?: "left" | "right" }>, + values: Array<{ text: string; align?: "left" | "right" }>, + x: number, +): number { + let cx = x; + for (let i = 0; i < cols.length; i++) { + const c = cols[i]; + const v = values[i] ?? { text: "" }; + const align = v.align ?? c.align ?? "left"; + const w = c.width; + doc + .font("Helvetica") + .fontSize(9) + .fillColor("#211d17") + .text(v.text, cx, y, { + width: w - 4, + align, + ellipsis: true, + lineBreak: false, + height: 16, + }); + cx += w; + } + return y + 18; +} + +export function renderPdf( + columns: ColumnDef[], + result: ReportResult, + title: string, +): Promise { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: "LETTER", + layout: "landscape", + margins: { top: 96, bottom: 56, left: 48, right: 48 }, + bufferPages: true, + info: { + Title: title, + Author: "Jorge Cuadros & Asociados", + Subject: "Reporte", + Creator: "Jorge Cuadros Platform — Reports module", + }, + }); + const chunks: Buffer[] = []; + doc.on("data", (c: Buffer) => chunks.push(c)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const company = getCompanyInfo(); + const pageW = doc.page.width - 96; + + /** The header is repeated on every page (via addPage + manual draw). */ + const drawHeader = () => { + // Background bar (brand pine) for the masthead. + doc.rect(0, 0, doc.page.width, 60).fill(BRAND); + + // Logo, fitted to a 40px box, with 8px padding. + let textX = 48; + if (company.logo) { + const targetH = 40; + const scale = targetH / company.logo.height; + const w = company.logo.width * scale; + doc.image(company.logo.buffer, 48, 10, { height: targetH }); + textX = 48 + w + 14; + } + + // Company name (large) + "Reporte" tag below it. + doc + .fillColor("#f5f1e8") + .font("Helvetica-Bold") + .fontSize(15) + .text(company.name, textX, 14, { width: pageW - (textX - 48), lineBreak: false }); + doc + .font("Helvetica") + .fontSize(8) + .fillColor("#cde0db") + .text("Reporte", textX, 36, { lineBreak: false }); + + // Right-aligned company locator (address + phone + email). + const rightLines = [ + company.addressLine1, + company.addressLine2, + [company.cityState].filter(Boolean).join(" · "), + [company.phone, company.email].filter(Boolean).join(" · "), + company.taxId ? `RFC: ${company.taxId}` : "", + ].filter(Boolean); + doc.font("Helvetica").fontSize(8).fillColor("#cde0db"); + let ry = 12; + for (const line of rightLines) { + doc.text(line, 48, ry, { + width: pageW, + align: "right", + lineBreak: false, + ellipsis: true, + }); + ry += 10; + } + + // Thin accent line under the masthead. + doc.rect(0, 60, doc.page.width, 2).fill(ACCENT); + + // Title + subtitle + printed-at. + doc + .font("Helvetica-Bold") + .fontSize(15) + .fillColor(BRAND) + .text(title, 48, 72, { lineBreak: false }); + let metaY = 92; + if (result.subtitle) { + doc + .font("Helvetica") + .fontSize(9) + .fillColor(MUTED) + .text(result.subtitle, 48, metaY, { lineBreak: false }); + metaY += 12; + } + const printedAt = new Date().toLocaleString("es-MX"); + doc + .font("Helvetica") + .fontSize(8) + .fillColor(MUTED) + .text(`Impreso: ${printedAt}`, 48, metaY, { lineBreak: false }); + }; + + drawHeader(); + + // Column widths: distribute page width minus margins, weighted. + const totalW = columns.reduce((s, c) => s + (c.width ?? 12), 0); + const cols = columns.map((c) => ({ + label: c.label, + width: ((c.width ?? 12) / totalW) * pageW, + align: c.align, + })); + + let y = 130; + const drawTableHeader = () => { + doc.rect(48, y, pageW, 18).fill("#faf6ee"); + y = pdfRow( + doc, + y + 4, + cols, + cols.map((c) => ({ text: c.label, align: c.align })), + 48, + ); + doc + .moveTo(48, y) + .lineTo(48 + pageW, y) + .strokeColor(LINE) + .lineWidth(0.5) + .stroke(); + }; + drawTableHeader(); + + // Body rows. + for (const r of result.rows) { + if (y > doc.page.height - 64) { + doc.addPage({ layout: "landscape", margins: { top: 96, bottom: 56, left: 48, right: 48 } }); + drawHeader(); + y = 130; + drawTableHeader(); + } + const vals = columns.map((c) => { + const v = r[c.key]; + const text = c.type === "money" ? fmtMoney(v) : v == null ? "" : String(v); + return { text, align: c.align }; + }); + y = pdfRow(doc, y + 4, cols, vals, 48); + doc + .moveTo(48, y) + .lineTo(48 + pageW, y) + .strokeColor("#e4dccb") + .lineWidth(0.4) + .stroke(); + } + + // Totals. + if (result.totals) { + y += 6; + doc.rect(48, y, pageW, 18).fill(ACCENT); + doc + .font("Helvetica-Bold") + .fontSize(9) + .fillColor("#f5f1e8") + .text( + Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · "), + 52, + y + 5, + { width: pageW - 8, align: "left" }, + ); + } + + doc.end(); + }); +} + +/* ----------------------------------------------------------------- print (HTML) */ + +/** + * Print-stylesheet-friendly HTML. The web app's print stylesheet hides + * nav, but otherwise this is a plain table the browser paginates itself. + * + * The header carries the company info (logo + name + locator) so printed + * pages stand alone — staff can hand one to a customer and the office + * identification is on every sheet, not buried in the cover page. + */ +export function renderPrintHtml( + columns: ColumnDef[], + result: ReportResult, + title: string, +): string { + const company = getCompanyInfo(); + const head = (label: string, align?: "left" | "right") => + `${escapeHtml(label)}`; + const cell = (v: unknown, c: ColumnDef) => { + const text = c.type === "money" ? fmtMoney(v) : v == null ? "" : String(v); + const align = c.align ?? "left"; + return `${escapeHtml(text)}`; + }; + const rows = result.rows + .map( + (r) => + `${columns + .map((c) => cell(r[c.key], c)) + .join("")}`, + ) + .join(""); + const totals = result.totals + ? `${Object.entries( + result.totals, + ) + .map(([k, v]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`) + .join("  ·  ")}` + : ""; + + // Logo embedded as base64 data URL — the print page is opened as a + // new tab and printed standalone, so a relative path to the web app + // wouldn't resolve when launched outside the web's origin. + const logoDataUrl = company.logo + ? `data:image/png;base64,${company.logo.buffer.toString("base64")}` + : null; + + const locatorLines = [ + company.addressLine1, + company.addressLine2, + company.cityState, + [company.phone, company.email].filter(Boolean).join(" · "), + company.taxId ? `RFC: ${company.taxId}` : "", + ].filter(Boolean); + + return ` + + +${escapeHtml(title)} — ${escapeHtml(company.name)} + + +

+
+ ${logoDataUrl ? `` : ""} +
+
${escapeHtml(company.name)}
+
Reporte
+
+
+ ${locatorLines.map((l) => escapeHtml(l)).join("
")} + ${company.website ? `
${escapeHtml(company.website)}` : ""} +
+
+
+
+

${escapeHtml(title)}

+ ${result.subtitle ? `

${escapeHtml(result.subtitle)}

` : ""} +

Impreso: ${new Date().toLocaleString("es-MX")}

+
+ + ${columns.map((c) => head(c.label, c.align)).join("")} + ${rows}${totals} +
+ +`; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/apps/api/src/reports/reports.controller.ts b/apps/api/src/reports/reports.controller.ts new file mode 100644 index 0000000..8dcd1b5 --- /dev/null +++ b/apps/api/src/reports/reports.controller.ts @@ -0,0 +1,130 @@ +import { + Body, + Controller, + Get, + Header, + Param, + Post, + Query, + Res, + UseGuards, +} from "@nestjs/common"; +import type { Response } from "express"; +import { AuthenticatedGuard } from "../auth/authenticated.guard"; +import { ReportsService } from "./reports.service"; +import { + renderCsv, + renderPdf, + renderPrintHtml, + renderXlsx, +} from "./outputs"; +import { findReport } from "./reports.registry"; + +/** + * Reports routes. Every report is dispatched by slug; outputs are + * differentiated by `?format=...` (default `json`). Reads only — gated + * by AuthenticatedGuard alone, like every other read in the app. + */ +@UseGuards(AuthenticatedGuard) +@Controller("reports") +export class ReportsController { + constructor(private readonly reports: ReportsService) {} + + /** Catalog of all registered reports (the /reportes index). */ + @Get() + catalog() { + return { items: this.reports.catalog() }; + } + + /** Run a report and return the JSON result (rows + totals + the def's columns). */ + @Get(":slug") + async runJson( + @Param("slug") slug: string, + @Query() query: Record, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + return { ...result, columns: def?.columns ?? [] }; + } + + /** CSV download. */ + @Get(":slug/csv") + @Header("Content-Type", "text/csv; charset=utf-8") + async runCsv( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.csv`; + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(renderCsv(def?.columns ?? [], result)); + } + + /** XLSX download. */ + @Get(":slug/xlsx") + async runXlsx( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.xlsx`; + const buf = await renderXlsx(def?.columns ?? [], result); + res.setHeader( + "Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(buf); + } + + /** PDF download. */ + @Get(":slug/pdf") + async runPdf( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const buf = await renderPdf(def?.columns ?? [], result, def?.title ?? slug); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(buf); + } + + /** Browser-printable HTML view (the user hits Print → Save as PDF). */ + @Get(":slug/print") + @Header("Content-Type", "text/html; charset=utf-8") + async runPrint( + @Param("slug") slug: string, + @Query() query: Record, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + return renderPrintHtml(def?.columns ?? [], result, def?.title ?? slug); + } + + /** POST a customer-picker-driven report (statement). Mirrors GET to keep + * the param contract simple: same body shape, same response. */ + @Post(":slug") + async runPost( + @Param("slug") slug: string, + @Body() body: Record, + ) { + return this.reports.run(slug, body); + } +} diff --git a/apps/api/src/reports/reports.module.ts b/apps/api/src/reports/reports.module.ts new file mode 100644 index 0000000..e13cbd7 --- /dev/null +++ b/apps/api/src/reports/reports.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { ReportsController } from "./reports.controller"; +import { ReportsService } from "./reports.service"; + +@Module({ + controllers: [ReportsController], + providers: [ReportsService], +}) +export class ReportsModule {} diff --git a/apps/api/src/reports/reports.registry.ts b/apps/api/src/reports/reports.registry.ts new file mode 100644 index 0000000..d759e97 --- /dev/null +++ b/apps/api/src/reports/reports.registry.ts @@ -0,0 +1,763 @@ +/** + * The catalog. One entry per report. Adding a new report is one new entry + * here — no new route, no new page, no new component. + * + * Filter behavior to keep in mind: + * - Currency balances are NEVER summed across currencies (912 customers + * carry both MXN and USD; the legacy data has no FX per row, so any + * cross-currency total would be invented). Every report that touches + * the ledger accepts a `currency` filter and reports per currency. + * - Voided transactions must be excluded from totals (NOT_VOIDED). The + * UI still shows them struck-through; the SQL drops them. + * - The legacy `REPORTE DE EFECTIVO` covered cash receipts only. In the + * new schema those are `Transaction` rows with `legacySourceTable` in + * the EFECTIVO* set OR `checkNumber` null + amount > 0 (true cash). + */ + +import { Prisma } from "@jorgecuadros/database"; +import { + intParam, + NOT_VOIDED, + parseDate, + type ReportDef, +} from "./reports.types"; + +/* ------------------------------------------------------------------ helpers */ + +function nameOf(c: { name: string; nameMissing: boolean }): string { + return c.nameMissing ? "(sin nombre)" : c.name; +} + +/* ------------------------------------------------------------------ reports */ + +/** + * LISTADO EN ROJO — overdue customers worklist. + * Same data as the receivables worklist with balance=owing, but presented + * as a printable report rather than a paginated browser. + */ +const listadoEnRojo: ReportDef = { + slug: "listado-en-rojo", + title: "Clientes en rojo", + description: + "Cartera vencida: clientes con saldo deudor en la moneda seleccionada, " + + "ordenados del más antiguo al más reciente.", + domain: "estado-cuenta", + legacyName: "LISTADO EN ROJO", + format: "tabular", + params: [ + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + { + key: "query", + label: "Buscar (nombre o ciudad)", + kind: "text", + placeholder: "Ej. Pérez, Tijuana…", + }, + ], + columns: [ + { key: "id", label: "#", type: "text" }, + { key: "name", label: "Cliente", type: "text" }, + { key: "city", label: "Ciudad", type: "text" }, + { key: "movements", label: "Movs.", type: "number", align: "right" }, + { key: "balance", label: "Saldo", type: "money", align: "right" }, + { key: "lastMovement", label: "Último movimiento", type: "date" }, + ], + async run(prisma, p) { + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + const q = p.query?.trim(); + const nameFilter = q + ? Prisma.sql`AND (c.name LIKE ${`%${q}%`} OR c.city LIKE ${`%${q}%`})` + : Prisma.empty; + + const bal = + currency === "USD" + ? Prisma.sql`SUM(CASE WHEN t.currency = 'USD' THEN t.amount ELSE 0 END)` + : Prisma.sql`SUM(CASE WHEN t.currency = 'MXN' THEN t.amount ELSE 0 END)`; + + const rows = await prisma.$queryRaw< + Array<{ + id: string; + name: string; + nameMissing: boolean; + city: string | null; + movements: bigint | number | string; + balance: Prisma.Decimal | null; + lastMovement: Date | null; + }> + >` + SELECT c.id, c.name, c.nameMissing, c.city, + COUNT(*) AS movements, + ${bal} AS balance, + MAX(t.transactionDate) AS lastMovement + FROM customers c + JOIN transactions t ON t.customerId = c.id + WHERE t.voidedAt IS NULL ${nameFilter} + GROUP BY c.id, c.name, c.nameMissing, c.city + HAVING ${bal} < -0.005 + ORDER BY MAX(t.transactionDate) ASC, c.nameMissing ASC, c.name ASC + `; + + let totalBalance = new Prisma.Decimal(0); + let totalMovs = 0; + const out = rows.map((r) => { + const b = r.balance ?? new Prisma.Decimal(0); + totalBalance = totalBalance.plus(b); + totalMovs += Number(r.movements); + return { + id: r.id.slice(0, 8), + name: nameOf(r), + city: r.city ?? "—", + movements: Number(r.movements), + balance: b.toFixed(2), + lastMovement: r.lastMovement + ? r.lastMovement.toISOString().slice(0, 10) + : "—", + }; + }); + + return { + rows: out, + totals: { + customers: out.length, + movements: totalMovs, + balance: totalBalance.toFixed(2), + currency, + }, + subtitle: `Moneda: ${currency} · ${out.length} clientes con saldo deudor`, + }; + }, +}; + +/** + * PAGOS NO EFECTUADOS (AGUA / LUZ / TEL). + * Customers enrolled in a service (by PropertyService.kind) with no + * related ledger charge in the last N days. Heuristic: any non-voided + * charge-type transaction in the window counts as "they paid". The + * report groups by property so the same customer with two water meters + * appears once per property. + */ +const pagosNoEfectuados: ReportDef = { + slug: "pagos-no-efectuados", + title: "Pagos no efectuados", + description: + "Clientes con un servicio contratado (agua, luz o teléfono) sin " + + "movimientos de cargo en los últimos N días. Heurística basada en el " + + "servicio registrado en la propiedad y la ausencia de cargos en el " + + "periodo seleccionado.", + domain: "servicios", + legacyName: "PAGOS NO EFECTUADOS AGUA/LUZ/TEL", + format: "tabular", + params: [ + { + key: "serviceKind", + label: "Servicio", + kind: "select", + options: [ + { value: "WATER", label: "Agua" }, + { value: "ELECTRICITY", label: "Luz" }, + { value: "PHONE", label: "Teléfono" }, + ], + defaultValue: "WATER", + }, + { + key: "days", + label: "Días sin movimiento", + kind: "number", + defaultValue: "60", + }, + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + ], + columns: [ + { key: "customerId", label: "Cliente #", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "propertyAddress", label: "Dirección", type: "text" }, + { key: "accountNumber", label: "Cuenta / Medidor", type: "text" }, + { key: "lastCharge", label: "Último cargo", type: "date" }, + { key: "balance", label: "Saldo", type: "money", align: "right" }, + ], + async run(prisma, p) { + const kind = (p.serviceKind ?? "WATER") as + | "WATER" + | "ELECTRICITY" + | "PHONE"; + const days = intParam(p, "days", 60, 1, 365); + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + const cutoff = new Date(Date.now() - days * 86400000); + + // Customers enrolled in the service on a non-archived property, with no + // charge-type transaction in the window. The subquery picks up + // *anything* the customer paid (any domain, any type) — close enough + // for the staff's "who's overdue" view; the precise per-service match + // would need a per-service typeId taxonomy that doesn't exist in the + // legacy data. + const rows = await prisma.$queryRaw< + Array<{ + customerId: string; + customerName: string; + nameMissing: boolean; + propertyId: string; + propertyAddress: string | null; + accountNumber: string | null; + lastCharge: Date | null; + balance: Prisma.Decimal | null; + }> + >` + SELECT + c.id AS customerId, + c.name AS customerName, + c.nameMissing AS nameMissing, + pr.id AS propertyId, + pr.addressLine1 AS propertyAddress, + ps.accountNumber AS accountNumber, + (SELECT MAX(t.transactionDate) FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.amount < 0 + AND t.transactionDate >= ${cutoff}) AS lastCharge, + (SELECT SUM(t.amount) FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.currency = ${currency}) AS balance + FROM property_services ps + JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL + JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL + WHERE ps.kind = ${kind} AND ps.active = 1 + AND NOT EXISTS ( + SELECT 1 FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.amount < 0 AND t.transactionDate >= ${cutoff} + ) + ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC + `; + + let totalBalance = new Prisma.Decimal(0); + const out = rows.map((r) => { + const b = r.balance ?? new Prisma.Decimal(0); + totalBalance = totalBalance.plus(b); + return { + customerId: r.customerId.slice(0, 8), + customerName: nameOf({ + name: r.customerName, + nameMissing: r.nameMissing, + }), + propertyAddress: r.propertyAddress ?? "—", + accountNumber: r.accountNumber ?? "—", + lastCharge: r.lastCharge + ? r.lastCharge.toISOString().slice(0, 10) + : "—", + balance: b.toFixed(2), + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + balance: totalBalance.toFixed(2), + currency, + }, + subtitle: `Servicio: ${ + kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono" + } · ${days} días · ${out.length} propiedades sin cargo reciente`, + }; + }, +}; + +/** + * FALTANTES DE (AGUA / LUZ / TEL). + * Data-quality report: properties enrolled in a service that are missing + * the key identifier the legacy system required (account/meter/route). + * Different `faltante` per service kind in the legacy because the + * service's identifier fields differ; here we flag any of the three + * common identifiers being blank. + */ +const faltantes: ReportDef = { + slug: "faltantes", + title: "Faltantes de datos por servicio", + description: + "Calidad de datos: propiedades con un servicio contratado que no " + + "tienen número de cuenta, medidor o ruta registrado. El reporte que " + + "en la legacy corría como FALTANTES DE AGUA / LUZ / TEL.", + domain: "servicios", + legacyName: "FALTANTES DE AGUA/LUZ/TEL", + format: "tabular", + params: [ + { + key: "serviceKind", + label: "Servicio", + kind: "select", + options: [ + { value: "WATER", label: "Agua" }, + { value: "ELECTRICITY", label: "Luz" }, + { value: "PHONE", label: "Teléfono" }, + ], + defaultValue: "WATER", + }, + ], + columns: [ + { key: "customerId", label: "Cliente #", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "propertyAddress", label: "Dirección", type: "text" }, + { key: "missing", label: "Faltante", type: "text" }, + { key: "dueDay", label: "Día de vencimiento", type: "text" }, + ], + async run(prisma, p) { + const kind = (p.serviceKind ?? "WATER") as + | "WATER" + | "ELECTRICITY" + | "PHONE"; + + // A row per (property, missing field). The "missing" string describes + // what's blank so the report is self-explanatory when printed. + const rows = await prisma.$queryRaw< + Array<{ + customerId: string; + customerName: string; + nameMissing: boolean; + propertyId: string; + propertyAddress: string | null; + dueDay: string | null; + missing: string; + }> + >` + SELECT + c.id AS customerId, + c.name AS customerName, + c.nameMissing AS nameMissing, + pr.id AS propertyId, + pr.addressLine1 AS propertyAddress, + ps.dueDay AS dueDay, + CASE + WHEN ps.accountNumber IS NULL OR ps.accountNumber = '' THEN 'Sin número de cuenta' + WHEN ps.meterNumber IS NULL OR ps.meterNumber = '' THEN 'Sin número de medidor' + WHEN ps.route IS NULL OR ps.route = '' THEN 'Sin ruta' + ELSE '' + END AS missing + FROM property_services ps + JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL + JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL + WHERE ps.kind = ${kind} AND ps.active = 1 + AND ( + ps.accountNumber IS NULL OR ps.accountNumber = '' + OR ps.meterNumber IS NULL OR ps.meterNumber = '' + OR ps.route IS NULL OR ps.route = '' + ) + ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC + `; + + const out = rows.map((r) => ({ + customerId: r.customerId.slice(0, 8), + customerName: nameOf({ + name: r.customerName, + nameMissing: r.nameMissing, + }), + propertyAddress: r.propertyAddress ?? "—", + missing: r.missing, + dueDay: r.dueDay ?? "—", + })); + + return { + rows: out, + totals: { rows: out.length }, + subtitle: `Servicio: ${ + kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono" + } · ${out.length} propiedades con datos faltantes`, + }; + }, +}; + +/** + * REPORTE DE EFECTIVO — cash reconciliation. + * Credits in the EFECTIVO* legacy source tables OR with no cheque number + * (true cash) within a date range. Excludes voided rows. Matches the + * shape of the legacy REPORTE DE EFECTIVO report. + */ +const reporteDeEfectivo: ReportDef = { + slug: "reporte-de-efectivo", + title: "Reporte de efectivo", + description: + "Recibos de efectivo en el periodo seleccionado. Cubre los abonos " + + "provenientes de las tablas legacy EFECTIVO* y los créditos sin " + + "número de cheque (efectivo real). El match del reporte original.", + domain: "chequera", + legacyName: "REPORTE DE EFECTIVO", + format: "tabular", + params: [ + { key: "from", label: "Desde", kind: "date" }, + { key: "to", label: "Hasta", kind: "date", endOfDay: true }, + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + ], + columns: [ + { key: "date", label: "Fecha", type: "date" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "concept", label: "Concepto", type: "text" }, + { key: "source", label: "Origen", type: "text" }, + { key: "amount", label: "Monto", type: "money", align: "right" }, + ], + async run(prisma, p) { + const from = parseDate(p.from); + const to = parseDate(p.to, true); + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + + const ands: Prisma.TransactionWhereInput[] = [ + NOT_VOIDED, + { amount: { gt: 0 } }, + { currency }, + { + OR: [ + { legacySourceTable: { in: ["EFECTIVO", "EFECTIVO_BACKUP"] } }, + { + AND: [ + { checkNumber: null }, + { legacySourceTable: { not: "CHEQUE FM3" } }, + ], + }, + ], + }, + ]; + if (from || to) { + ands.push({ + transactionDate: { + ...(from ? { gte: from } : {}), + ...(to ? { lte: to } : {}), + }, + }); + } + + const rows = await prisma.transaction.findMany({ + where: { AND: ands }, + orderBy: { transactionDate: "asc" }, + select: { + transactionDate: true, + amount: true, + reference: true, + checkNumber: true, + message: true, + legacySourceTable: true, + type: { select: { nameEs: true, nameEn: true } }, + customer: { select: { name: true, nameMissing: true } }, + }, + }); + + let total = new Prisma.Decimal(0); + const out = rows.map((r) => { + total = total.plus(r.amount); + return { + date: r.transactionDate.toISOString().slice(0, 10), + customerName: nameOf(r.customer), + concept: r.message ?? r.type?.nameEs ?? r.type?.nameEn ?? "—", + source: r.legacySourceTable ?? "—", + amount: r.amount.toFixed(2), + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + total: total.toFixed(2), + currency, + }, + subtitle: `Efectivo · ${currency} · ${out.length} recibos${ + from ? ` desde ${p.from}` : "" + }${to ? ` hasta ${p.to}` : ""}`, + }; + }, +}; + +/** + * VIGENTE (LIC / INCEN / MULT / …) — policies up for renewal. + * Wraps the policy listing with status=expiring and a policy-type filter, + * sorted by soonest expiry. The legacy VIGENTE LIC / INCEN / MULT + * reports are the same data; the new filter is a dropdown. + */ +const vigente: ReportDef = { + slug: "vigente", + title: "Pólizas por vencer", + description: + "Pólizas que vencen en los próximos N días, filtradas por ramo. " + + "Equivalente a los reportes VIGENTE LIC / INCEN / MULT de la legacy.", + domain: "polizas", + legacyName: "VIGENTE LIC/INCEN/MULT", + format: "tabular", + params: [ + { + key: "typeName", + label: "Ramo", + kind: "select", + options: [ + { value: "LICENCIAS", label: "Licencias" }, + { value: "INCENDIO", label: "Incendio" }, + { value: "MULT", label: "Multirriesgo" }, + { value: "MCA2", label: "MCA2 (auto)" }, + { value: "ME", label: "ME" }, + { value: "MF", label: "MF" }, + { value: "RC", label: "RC" }, + { value: "INCEN", label: "Incen" }, + { value: "TAMPL", label: "TAMPL" }, + { value: "FAMILIAR", label: "Familiar" }, + ], + defaultValue: "LICENCIAS", + }, + { + key: "days", + label: "Ventana (días)", + kind: "number", + defaultValue: "30", + }, + ], + columns: [ + { key: "policyNumber", label: "Póliza", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "provider", label: "Aseguradora", type: "text" }, + { key: "agent", label: "Agente", type: "text" }, + { key: "from", label: "Desde", type: "date" }, + { key: "to", label: "Vence", type: "date" }, + { key: "daysToExpire", label: "Días", type: "number", align: "right" }, + { key: "premium", label: "Prima neta", type: "money", align: "right" }, + ], + async run(prisma, p) { + const typeName = p.typeName ?? "LICENCIAS"; + const days = intParam(p, "days", 30, 1, 365); + const now = new Date(); + const today = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()), + ); + const soon = new Date(today.getTime() + days * 86400000); + + const rows = await prisma.policy.findMany({ + where: { + policyType: { name: typeName }, + archivedAt: null, + policyTo: { gte: today, lte: soon }, + }, + orderBy: { policyTo: "asc" }, + select: { + policyNumber: true, + policyFrom: true, + policyTo: true, + netPremium: true, + agentName: true, + customer: { select: { name: true, nameMissing: true } }, + insuranceProvider: { select: { name: true } }, + }, + }); + + let totalPremium = new Prisma.Decimal(0); + const out = rows.map((r) => { + const daysTo = r.policyTo + ? Math.round( + (r.policyTo.getTime() - today.getTime()) / 86400000, + ) + : 0; + if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium); + return { + policyNumber: r.policyNumber, + customerName: nameOf(r.customer), + provider: r.insuranceProvider?.name ?? "—", + agent: r.agentName ?? "—", + from: r.policyFrom ? r.policyFrom.toISOString().slice(0, 10) : "—", + to: r.policyTo ? r.policyTo.toISOString().slice(0, 10) : "—", + daysToExpire: daysTo, + premium: r.netPremium ? r.netPremium.toFixed(2) : "0.00", + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + premium: totalPremium.toFixed(2), + }, + subtitle: `Ramo: ${typeName} · ${days} días · ${out.length} pólizas por vencer`, + }; + }, +}; + +/** + * EDO CUENTA DATOS — per-customer account statement. + * Wraps the existing BillingService.statement() output. The full layout + * (header, balance per currency, by-domain split, by-type breakdown, + * full movement list with running balance) is rendered by the statement + * page; this report is the same data with print/PDF/CSV/XLSX outputs. + */ +const edoCuentaDatos: ReportDef = { + slug: "edo-cuenta-datos", + title: "Estado de cuenta", + description: + "Estado de cuenta de un cliente: saldos por moneda, desglose por " + + "ramo y concepto, y el historial completo de movimientos con saldo " + + "corrido. El reporte del cliente final.", + domain: "estado-cuenta", + legacyName: "EDO CUENTA DATOS", + format: "statement", + params: [ + { key: "customerId", label: "Cliente", kind: "customer-picker" }, + ], + columns: [ + // Statement rows carry synthetic `__kind` discriminators instead of + // column keys; the runner renders the special cases inline. These + // columns drive CSV/XLSX when the user wants a flat movement export. + { key: "date", label: "Fecha", type: "date" }, + { key: "concept", label: "Concepto", type: "text" }, + { key: "reference", label: "Referencia", type: "text" }, + { key: "amount", label: "Cargo / Abono", type: "money", align: "right" }, + { key: "balanceAfter", label: "Saldo", type: "money", align: "right" }, + ], + async run(prisma, p) { + const customerId = p.customerId; + if (!customerId) { + return { rows: [], subtitle: "Selecciona un cliente" }; + } + const customer = await prisma.customer.findUnique({ + where: { id: customerId }, + select: { + id: true, + name: true, + nameMissing: true, + addressLine1: true, + city: true, + state: true, + email: true, + phone: true, + }, + }); + if (!customer) return { rows: [], subtitle: "Cliente no encontrado" }; + + // Reuse the same NOT_VOIDED + STATEMENT_EXCLUDED_SOURCE_TABLES filter + // as BillingService.statement so the numbers match what the customer + // already sees in /estado-cuenta/[id]. + const rows = await prisma.transaction.findMany({ + where: { + customerId, + voidedAt: null, + legacySourceTable: { + notIn: [ + "EFECTIVO", + "EFECTIVO_BACKUP", + "EFECTIVO FM3", + "CHEQUE FM3", + "IVA 2015", + ], + }, + }, + orderBy: [{ transactionDate: "asc" }, { id: "asc" }], + select: { + id: true, + transactionDate: true, + domain: true, + amount: true, + currency: true, + reference: true, + period: true, + checkNumber: true, + message: true, + legacySourceTable: true, + type: { select: { nameEs: true, nameEn: true } }, + }, + }); + + // Compute running balance per currency, then return newest-first. + const running = new Map(); + const movements = rows.map((r) => { + const prev = running.get(r.currency) ?? new Prisma.Decimal(0); + const next = prev.plus(r.amount); + running.set(r.currency, next); + return { + date: r.transactionDate.toISOString().slice(0, 10), + domain: r.domain, + currency: r.currency, + reference: r.reference ?? "", + period: r.period ?? "", + checkNumber: r.checkNumber ?? "", + concept: r.type?.nameEs ?? r.type?.nameEn ?? "—", + amount: r.amount.toFixed(2), + balanceAfter: next.toFixed(2), + }; + }); + movements.reverse(); + + // Per-currency summary + per-domain breakdown. + const perCurrency = new Map< + string, + { currency: string; charges: Prisma.Decimal; credits: Prisma.Decimal; count: number } + >(); + for (const r of rows) { + const c = + perCurrency.get(r.currency) ?? + { + currency: r.currency, + charges: new Prisma.Decimal(0), + credits: new Prisma.Decimal(0), + count: 0, + }; + c.count += 1; + if (r.amount.lessThan(0)) c.charges = c.charges.plus(r.amount); + else c.credits = c.credits.plus(r.amount); + perCurrency.set(r.currency, c); + } + + return { + rows: [ + { + __kind: "header", + name: nameOf(customer), + address: customer.addressLine1 ?? "", + city: [customer.city, customer.state].filter(Boolean).join(", "), + phone: customer.phone ?? "", + email: customer.email ?? "", + }, + ...[...perCurrency.values()].map((c) => ({ + __kind: "summary", + currency: c.currency, + charges: c.charges.toFixed(2), + credits: c.credits.toFixed(2), + balance: c.charges.plus(c.credits).toFixed(2), + count: c.count, + })), + { __kind: "movements-header" }, + ...movements, + ], + subtitle: `${nameOf(customer)} · ${rows.length} movimientos`, + }; + }, +}; + +/* ------------------------------------------------------------------ export */ + +export const REPORTS: ReportDef[] = [ + listadoEnRojo, + pagosNoEfectuados, + faltantes, + reporteDeEfectivo, + vigente, + edoCuentaDatos, +]; + +export function findReport(slug: string): ReportDef | undefined { + return REPORTS.find((r) => r.slug === slug); +} diff --git a/apps/api/src/reports/reports.service.ts b/apps/api/src/reports/reports.service.ts new file mode 100644 index 0000000..535dab7 --- /dev/null +++ b/apps/api/src/reports/reports.service.ts @@ -0,0 +1,45 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { findReport, REPORTS } from "./reports.registry"; +import type { ReportDef } from "./reports.types"; + +/** + * The reports service. Two responsibilities: + * 1. Run a report by slug with the given params — just dispatch. + * 2. Return the catalog for the /reportes index page. + * + * Output rendering (CSV/XLSX/PDF/HTML print) lives in `outputs.ts`; this + * service is data only. The controller maps URLs to (slug, format) and + * hands the result to outputs. + */ +@Injectable() +export class ReportsService { + constructor(private readonly prisma: PrismaService) {} + + /** List every registered report, in display order. */ + catalog(): Array<{ + slug: string; + title: string; + description: string; + domain: string; + legacyName: string | null; + format: string; + params: ReportDef["params"]; + }> { + return REPORTS.map((r) => ({ + slug: r.slug, + title: r.title, + description: r.description, + domain: r.domain, + legacyName: r.legacyName, + format: r.format, + params: r.params, + })); + } + + async run(slug: string, params: Record) { + const def = findReport(slug); + if (!def) throw new NotFoundException(`Reporte "${slug}" no encontrado`); + return def.run(this.prisma, params); + } +} diff --git a/apps/api/src/reports/reports.types.ts b/apps/api/src/reports/reports.types.ts new file mode 100644 index 0000000..a59549e --- /dev/null +++ b/apps/api/src/reports/reports.types.ts @@ -0,0 +1,140 @@ +/** + * The reports module — plan step 10. + * + * Each "report" is one entry in `reports.registry.ts`. The entry declares + * its slug (URL id), title, what filters it accepts, what columns it + * returns, and a `run` function that produces the data from Prisma. The + * service dispatches on slug; the controller exposes JSON + CSV + XLSX + + * PDF + HTML print; the catalog endpoint exposes the registry itself so + * the `/reportes` page can render the same data. + * + * Output philosophy: a report returns a uniform shape — `columns` (typed + * schema) + `rows` (any[] of values matching the column types) + `totals` + * (record of column key → summary value). All three output formats + * (CSV/XLSX/PDF/print) derive from this same shape so adding a new + * report is one entry, never a per-format template. + */ + +import { Prisma } from "@jorgecuadros/database"; +import { PrismaService } from "../prisma/prisma.service"; + +/** Top-level grouping for the catalog page; matches the existing nav. */ +export type ReportDomain = + | "clientes" + | "polizas" + | "servicios" + | "estado-cuenta" + | "chequera"; + +/** How the runner should render rows: a grid, or a per-customer statement. */ +export type ReportFormat = "tabular" | "statement"; + +/** Filter controls the report's UI should render. */ +export type ParamDef = + | { + key: string; + label: string; + kind: "text" | "number"; + placeholder?: string; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "date"; + /** Inclusive bound, true for `to`, false for `from`. */ + endOfDay?: boolean; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "select"; + options: { value: string; label: string }[]; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "customer-picker"; + }; + +/** One column of the output table. */ +export interface ColumnDef { + key: string; + label: string; + /** Render hint for the on-screen + print table. */ + type: "text" | "number" | "money" | "date"; + /** Right-align numbers/money; default false (left). */ + align?: "left" | "right"; + /** Used for column-width hints in the print/PDF layout. */ + width?: number; +} + +/** Shape every report's `run` resolves to. Columns come from the def. */ +export interface ReportResult { + rows: Array>; + totals?: Record; + /** Optional free-form subtitle for print/PDF (e.g. date range, scope). */ + subtitle?: string; +} + +/** A report's static declaration. */ +export interface ReportDef { + slug: string; + title: string; + description: string; + domain: ReportDomain; + /** The original Access report name (per docs/LEGACY_DATABASES_OBJECTS.md) + * for traceability. Null when this is a new report with no legacy equiv. */ + legacyName: string | null; + format: ReportFormat; + params: ParamDef[]; + columns: ColumnDef[]; + /** + * Run the report. Receives the Prisma client and the validated params + * record (keys are the `key` from ParamDef, values are the strings the + * runner collected; numeric/date params arrive as strings — the report + * parses them). Must apply the same NOT_VOIDED filter on transactions as + * the billing module so totals match. + */ + run: ( + prisma: PrismaService, + params: Record, + ) => Promise; +} + +/** A typed bag of helpers for the report functions. */ +export interface ReportCtx { + prisma: PrismaService; + params: Record; +} + +/** Helper: a `YYYY-MM-DD` bound; unparseable is undefined. */ +export function parseDate( + v: string | undefined, + endOfDay = false, +): Date | undefined { + if (!v) return undefined; + const d = new Date(endOfDay ? `${v}T23:59:59.999Z` : `${v}T00:00:00.000Z`); + return Number.isNaN(d.getTime()) ? undefined : d; +} + +/** Helper: integer param with default. */ +export function intParam( + p: Record, + key: string, + def: number, + min = 1, + max = 1000, +): number { + const n = Number(p[key]); + if (!Number.isFinite(n)) return def; + return Math.min(max, Math.max(min, Math.round(n))); +} + +/** Helper: not-voided filter, shared with billing.service. */ +export const NOT_VOIDED: Prisma.TransactionWhereInput = { voidedAt: null }; +export const NOT_VOIDED_BANK: Prisma.BankTransactionWhereInput = { + voidedAt: null, +}; diff --git a/apps/web/src/app/banco/page.tsx b/apps/web/src/app/banco/page.tsx index 83028fb..6cf0935 100644 --- a/apps/web/src/app/banco/page.tsx +++ b/apps/web/src/app/banco/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { createBankMovement, getBankFacets, @@ -208,6 +209,13 @@ function BankBrowser() { parte del estado de cuenta de los clientes y sus cifras no se suman con las de ellos.

+
+ +
diff --git a/apps/web/src/app/clientes/[id]/page.tsx b/apps/web/src/app/clientes/[id]/page.tsx index 3e1b49b..3a21d60 100644 --- a/apps/web/src/app/clientes/[id]/page.tsx +++ b/apps/web/src/app/clientes/[id]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { archiveCustomer, getCustomer, @@ -95,6 +96,15 @@ function Detail({ id }: { id: string }) {
+ getCustomer(id).then(setData).catch(() => {})} diff --git a/apps/web/src/app/clientes/page.tsx b/apps/web/src/app/clientes/page.tsx index 21ca29c..af89f0b 100644 --- a/apps/web/src/app/clientes/page.tsx +++ b/apps/web/src/app/clientes/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { getStats, listCustomers } from "@/lib/api"; import { useCan } from "@/lib/abilities"; import { formatNumber, SIN_NOMBRE } from "@/lib/labels"; @@ -99,6 +100,12 @@ function ClientesBrowser() {

Clientes

+ {canCreate && ( + Nuevo cliente diff --git a/apps/web/src/app/estado-cuenta/page.tsx b/apps/web/src/app/estado-cuenta/page.tsx index 853508c..d1b3259 100644 --- a/apps/web/src/app/estado-cuenta/page.tsx +++ b/apps/web/src/app/estado-cuenta/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { MovementForm } from "@/components/MovementForm"; import { getBillingFacets, @@ -256,6 +257,15 @@ function BillingBrowser() {
+
+ +
+
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index e442240..4340d75 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -2205,3 +2205,488 @@ button { padding: 20px; z-index: 50; } + +/* ============================================================================ + Home dashboard (/inicio) + ========================================================================== */ +.home { + display: flex; + flex-direction: column; + gap: 36px; + padding-top: 8px; +} +.home-last-seen { + margin-top: 14px; + font-size: 13px; +} +.home-section { + display: flex; + flex-direction: column; + gap: 14px; +} +.section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.section-title { + font-family: var(--font-display); + font-size: 20px; + font-weight: 560; + margin: 0; + color: var(--ink); + letter-spacing: -0.005em; +} +.section-sub { + font-size: 12.5px; + color: var(--muted); +} + +/* KPI cards row — 4 across on wide screens, 2 on tablet, 1 on mobile */ +.home-section.kpi-row { + display: grid; +} +.home > .home-section:first-of-type { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} +@media (max-width: 1080px) { + .home > .home-section:first-of-type { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 560px) { + .home > .home-section:first-of-type { + grid-template-columns: 1fr; + } +} + +.kpi-card { + display: flex; + flex-direction: column; + padding: 18px 18px 16px; + text-decoration: none; + color: inherit; + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; + position: relative; +} +.kpi-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.kpi-label { + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + font-weight: 700; +} +.kpi-primary { + font-family: var(--font-display); + font-size: 32px; + font-weight: 560; + color: var(--ink); + margin-top: 8px; + font-feature-settings: "tnum" 1; + letter-spacing: -0.015em; + line-height: 1.1; + min-height: 36px; +} +.kpi-sub { + list-style: none; + padding: 0; + margin: 12px 0 0; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12.5px; + color: var(--ink-soft); +} +.kpi-sub li { + line-height: 1.35; +} +.kpi-cta { + margin-top: 14px; + font-size: 12px; + font-weight: 700; + color: var(--brand-700); + letter-spacing: 0.02em; + text-transform: uppercase; +} +.kpi-card:hover .kpi-cta { + color: var(--brand-800); +} + +/* Attention grid — auto-fill so the row of cards stays balanced */ +.attention-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 14px; +} +.attention-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px 18px 18px; + text-decoration: none; + color: inherit; + border-left: 3px solid var(--line-strong); + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; + position: relative; +} +.attention-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.attention-card.tone-warn { + border-left-color: var(--seguros); +} +.attention-card.tone-info { + border-left-color: var(--brand-600); +} +.attention-card.tone-muted { + border-left-color: var(--muted-2); +} +.attention-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.attention-title { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + font-weight: 700; +} +.attention-arrow { + color: var(--muted-2); + font-size: 14px; + transition: transform 0.18s var(--ease-out-expo), color 0.18s; +} +.attention-card:hover .attention-arrow { + color: var(--brand-700); + transform: translateX(3px); +} +.attention-primary { + font-family: var(--font-display); + font-size: 26px; + font-weight: 560; + color: var(--ink); + font-feature-settings: "tnum" 1; + letter-spacing: -0.01em; + line-height: 1.1; + min-height: 30px; + margin-top: 2px; +} +.attention-sub { + font-size: 12.5px; + color: var(--ink-soft); + line-height: 1.4; +} +.attention-meta { + font-size: 11.5px; + color: var(--muted); + margin-top: 4px; +} + +/* Currency totals inside an attention card */ +.home-currency-totals { + display: inline-flex; + flex-wrap: wrap; + gap: 8px 14px; + align-items: baseline; +} +.home-currency-totals-row { + display: inline-flex; + align-items: baseline; + gap: 6px; +} +.home-currency-totals-num { + font-family: var(--font-display); + font-feature-settings: "tnum" 1; + font-size: 20px; + font-weight: 560; +} + +/* Quick links row */ +.quick-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; +} +.quick-link { + display: flex; + flex-direction: column; + padding: 14px 16px; + text-decoration: none; + color: inherit; + position: relative; + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; +} +.quick-link:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.quick-label { + font-family: var(--font-display); + font-size: 17px; + font-weight: 560; + color: var(--ink); +} +.quick-sub { + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} +.quick-arrow { + position: absolute; + top: 14px; + right: 16px; + color: var(--muted-2); + font-size: 14px; + transition: transform 0.18s var(--ease-out-expo), color 0.18s; +} +.quick-link:hover .quick-arrow { + color: var(--brand-700); + transform: translateX(3px); +} + +/* Money tone helpers (used in chequera card) */ +.money-pos { color: var(--positive); } +.money-neg { color: var(--negative); } + +/* ============================================================================ + Reports module + ========================================================================== */ + +.report-catalog { + display: flex; + flex-direction: column; + gap: 28px; + margin-top: 20px; +} +.report-catalog-group { + display: flex; + flex-direction: column; + gap: 10px; +} +.report-catalog-title { + font-family: var(--font-display); + font-weight: 500; + font-size: 20px; + color: var(--brand-800); + margin: 0; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); +} +.report-catalog-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 12px; +} +.report-catalog-item { + margin: 0; +} +.report-catalog-link { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 16px; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + color: inherit; + text-decoration: none; + transition: border-color 0.18s, transform 0.18s var(--ease-out-expo); +} +.report-catalog-link:hover { + border-color: var(--brand-500); + transform: translateY(-1px); +} +.report-catalog-item-title { + font-weight: 600; + font-size: 15px; + color: var(--ink); +} +.report-catalog-item-desc { + font-size: 13px; + color: var(--muted); + line-height: 1.4; +} +.report-catalog-item-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} +.report-catalog-tag { + font-size: 11px; + background: var(--brand-tint); + color: var(--brand-800); + padding: 2px 8px; + border-radius: 999px; + font-family: var(--font-mono); +} +.report-catalog-tag.muted { + background: var(--paper-2); + color: var(--muted); +} + +/* Runner */ +.report-runner { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 20px; +} +.report-filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px 16px; +} +.report-filters-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} +.report-error { + background: #fbeae3; + color: var(--negative); + border: 1px solid #e6b6a4; + border-radius: 6px; + padding: 10px 14px; + font-size: 13px; +} +.report-loading { + font-size: 12px; + color: var(--muted); + padding: 4px 0; +} +.report-result { + display: flex; + flex-direction: column; + gap: 12px; +} +.report-result-head { + display: flex; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; +} +.report-result-meta { + flex: 1; + min-width: 200px; +} +.report-output-buttons { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.btn-sm { + padding: 4px 10px; + font-size: 12px; +} +.report-table-wrap { + overflow-x: auto; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; +} +.report-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.report-table th, +.report-table td { + padding: 8px 12px; + border-bottom: 1px solid var(--line); +} +.report-table th { + background: var(--paper-2); + color: var(--ink-soft); + font-weight: 600; + text-align: left; + position: sticky; + top: 0; + z-index: 1; +} +.report-table tbody tr:hover { + background: var(--paper-2); +} +.report-totals { + background: var(--accent); + color: #f5f1e8; + font-weight: 600; + padding: 10px 14px; +} +.statement { + display: flex; + flex-direction: column; + gap: 20px; +} +.statement-head { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + padding: 16px 18px; +} +.statement-name { + font-family: var(--font-display); + font-size: 22px; + font-weight: 600; + color: var(--ink); + margin: 0 0 4px; +} +.statement-section-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 500; + color: var(--brand-800); + margin: 0 0 8px; +} + +/* Context buttons (the inline shortcut on existing pages) */ +.context-reports { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin-left: auto; +} +.context-reports-label { + font-size: 11px; + color: var(--muted-2); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-right: 4px; + font-weight: 500; +} +.context-report-link { + font-size: 12px; + padding: 4px 10px; + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink); + border-radius: 999px; + text-decoration: none; + transition: border-color 0.15s, color 0.15s; +} +.context-report-link:hover { + border-color: var(--brand-500); + color: var(--brand-700); +} diff --git a/apps/web/src/app/inicio/page.tsx b/apps/web/src/app/inicio/page.tsx new file mode 100644 index 0000000..db46d2f --- /dev/null +++ b/apps/web/src/app/inicio/page.tsx @@ -0,0 +1,455 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { AppShell } from "@/components/AppShell"; +import { + EXPIRY_WINDOW_DAYS, + getBankStats, + getBillingStats, + getPolicyStats, + getPropertyStats, + getStats, +} from "@/lib/api"; +import { useAuth } from "@/lib/abilities"; +import { + balancePhrase, + formatDate, + formatMoney, + formatNumber, + policyStatusLabel, + trustStatusLabel, +} from "@/lib/labels"; +import type { + BankStats, + BillingStats, + CustomerStats, + PolicyStats, + PropertyStats, +} from "@/lib/types"; + +export default function InicioPage() { + return ( + + + + ); +} + +interface DashboardData { + customers: CustomerStats | null; + policies: PolicyStats | null; + properties: PropertyStats | null; + billing: BillingStats | null; + bank: BankStats | null; +} + +function HomeDashboard() { + const user = useAuth(); + const [data, setData] = useState({ + customers: null, + policies: null, + properties: null, + billing: null, + bank: null, + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let alive = true; + Promise.allSettled([ + getStats(), + getPolicyStats(), + getPropertyStats(), + getBillingStats(), + getBankStats(), + ]).then((results) => { + if (!alive) return; + setData({ + customers: results[0].status === "fulfilled" ? results[0].value : null, + policies: results[1].status === "fulfilled" ? results[1].value : null, + properties: results[2].status === "fulfilled" ? results[2].value : null, + billing: results[3].status === "fulfilled" ? results[3].value : null, + bank: results[4].status === "fulfilled" ? results[4].value : null, + }); + setLoading(false); + }); + return () => { + alive = false; + }; + }, []); + + const greeting = greetingFor(user?.name); + const lastBillingMovement = data.billing?.lastMovement ?? null; + const lastBankMovement = data.bank?.lastMovement ?? null; + + return ( +
+
+

Resumen general

+

{greeting}

+

+ Vista rápida del estado de la cartera de clientes, las pólizas + activas, los fideicomisos y los movimientos recientes. +

+ +
+ +
+ + + + +
+ +
+
+

Atención

+ + Lo que conviene revisar antes de cerrar el día + +
+ +
+ + + + + ) : ( + "—" + ) + } + sub={ + data.billing + ? `En ${formatNumber(data.billing.ledgerCustomers)} expedientes con cargo` + : undefined + } + meta={ + data.billing + ? `${formatNumber(data.billing.crossLineCustomers)} clientes con cargo en ambos ramos` + : undefined + } + /> + + {formatMoney(data.bank.net, "MXN")} + + ) : ( + "—" + ) + } + sub={ + data.bank + ? balancePhrase(data.bank.net) + : undefined + } + meta={ + data.bank + ? `${formatNumber(data.bank.movements)} movimientos · ${formatNumber(data.bank.pending)} pendientes` + : undefined + } + /> +
+
+ +
+
+

Accesos rápidos

+ + Ir directo a cada módulo + +
+
+ + + + + +
+
+
+ ); +} + +function greetingFor(name?: string): string { + const hour = new Date().getHours(); + const partOfDay = + hour < 12 ? "Buenos días" : hour < 19 ? "Buenas tardes" : "Buenas noches"; + const display = (name ?? "").trim().split(/\s+/)[0]; + return display ? `${partOfDay}, ${display}` : partOfDay; +} + +function LastSeenLine({ + billing, + bank, + loading, +}: { + billing: string | null; + bank: string | null; + loading: boolean; +}) { + if (loading) { + return ( + + ); + } + if (!billing && !bank) return null; + return ( +

+ {billing && <>Último movimiento de cartera: {formatDate(billing)}} + {billing && bank && } + {bank && <>Último movimiento de chequera: {formatDate(bank)}} +

+ ); +} + +function KpiCard({ + href, + label, + primary, + sub, + loading, +}: { + href: string; + label: string; + primary: React.ReactNode; + sub: string[]; + loading: boolean; +}) { + return ( + +
{label}
+
+ {loading ? ( + + ) : ( + primary + )} +
+
    + {loading + ? Array.from({ length: 3 }).map((_, i) => ( +
  • + )) + : sub.map((line) =>
  • {line}
  • )} +
+ + + ); +} + +function AttentionCard({ + href, + tone, + title, + primary, + sub, + meta, + loading, +}: { + href: string; + tone: "warn" | "info" | "muted"; + title: string; + primary: React.ReactNode; + sub?: string; + meta?: string; + loading: boolean; +}) { + return ( + +
+ {title} + +
+
+ {loading ? ( + + ) : ( + primary + )} +
+ {sub && !loading &&
{sub}
} + {meta && !loading &&
{meta}
} + + ); +} + +function QuickLink({ + href, + label, + sub, +}: { + href: string; + label: string; + sub: string; +}) { + return ( + + {label} + {sub} + + + ); +} + +function CurrencyTotals({ + totals, + field, +}: { + totals: BillingStats["byCurrency"]; + field: "owing" | "inCredit"; +}) { + if (!totals || totals.length === 0) return <>—; + return ( + + {totals.map((c) => ( + + {c.currency} + + {formatNumber(c[field])} + + + ))} + + ); +} diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index 36f201b..7c23046 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -16,7 +16,7 @@ export default function LoginPage() { useEffect(() => { let alive = true; me() - .then(() => router.replace("/clientes")) + .then(() => router.replace("/inicio")) .catch(() => { if (alive) setBootChecking(false); }); @@ -31,7 +31,7 @@ export default function LoginPage() { setSubmitting(true); try { await login(email.trim(), password); - router.replace("/clientes"); + router.replace("/inicio"); } catch (err) { if (err instanceof ApiError && err.status === 401) { setError("Correo o contraseña incorrectos"); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 70a6814..b027fc5 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function HomePage() { - redirect("/clientes"); + redirect("/inicio"); } diff --git a/apps/web/src/app/polizas/[id]/page.tsx b/apps/web/src/app/polizas/[id]/page.tsx index b875881..5c26b8f 100644 --- a/apps/web/src/app/polizas/[id]/page.tsx +++ b/apps/web/src/app/polizas/[id]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { addPolicyChild, archivePolicy, @@ -92,6 +93,15 @@ function Detail({ id }: { id: string }) {
+
diff --git a/apps/web/src/app/polizas/page.tsx b/apps/web/src/app/polizas/page.tsx index e480f05..e16b548 100644 --- a/apps/web/src/app/polizas/page.tsx +++ b/apps/web/src/app/polizas/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { useCan } from "@/lib/abilities"; import { EXPIRY_WINDOW_DAYS, @@ -127,6 +128,13 @@ function PolizasBrowser() {

Pólizas

+ {canCreate && ( + Nueva póliza )} diff --git a/apps/web/src/app/reportes/[slug]/page.tsx b/apps/web/src/app/reportes/[slug]/page.tsx new file mode 100644 index 0000000..a71806f --- /dev/null +++ b/apps/web/src/app/reportes/[slug]/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AppShell } from "@/components/AppShell"; +import { ReportRunner } from "@/components/ReportRunner"; +import { getReportCatalog } from "@/lib/api"; +import type { ReportDef } from "@/lib/types"; + +/** + * /reportes/[slug] — one report's runner page. The whole page is + * data-driven from the catalog; if a slug isn't in the registry the + * page shows a "not found" inline message. + */ +export default function ReporteRunnerPage({ + params, + searchParams, +}: { + params: { slug: string }; + searchParams?: Record; +}) { + return ( + + + + ); +} + +function Runner({ + slug, + searchParams, +}: { + slug: string; + searchParams?: Record; +}) { + const [def, setDef] = useState(null); + const [missing, setMissing] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + getReportCatalog() + .then((c) => { + const found = c.items.find((r) => r.slug === slug); + if (found) setDef(found); + else setMissing(true); + }) + .catch((e) => setError(e?.message ?? "No se pudo cargar el reporte.")); + }, [slug]); + + if (error) { + return ( + <> + +
{error}
+ + ); + } + if (missing) { + return ( + <> + +
Reporte "{slug}" no encontrado.
+ + ); + } + if (!def) { + return ( + <> + +
Cargando reporte…
+ + ); + } + + const initialParams: Record = {}; + if (searchParams) { + const allowed = new Set(def.params.map((p) => p.key)); + for (const [k, v] of Object.entries(searchParams)) { + if (!allowed.has(k)) continue; + const s = Array.isArray(v) ? v[0] : v; + if (s) initialParams[k] = s; + } + } + + return ( + <> +
+

Reportes

+

{def.title}

+

+ {def.description} +

+ {def.legacyName && ( +

+ Equivalente en Access: {def.legacyName} +

+ )} +
+ + + ); +} + +function BackLink() { + return ( + + ← Reportes + + ); +} diff --git a/apps/web/src/app/reportes/page.tsx b/apps/web/src/app/reportes/page.tsx new file mode 100644 index 0000000..1c9dcb3 --- /dev/null +++ b/apps/web/src/app/reportes/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AppShell } from "@/components/AppShell"; +import { getReportCatalog } from "@/lib/api"; +import type { ReportCatalog, ReportDef, ReportDomain } from "@/lib/types"; + +/** + * The /reportes catalog. Index of every registered report, grouped by + * domain (matches the main nav). Discovery layer for the 280+ reports + * the system will eventually surface; for now we ship 6 in v1. + */ +export default function ReportesPage() { + return ( + + + + ); +} + +const DOMAIN_LABEL: Record = { + clientes: "Clientes", + polizas: "Pólizas", + servicios: "Servicios", + "estado-cuenta": "Estado de cuenta", + chequera: "Chequera", +}; + +const DOMAIN_ORDER: ReportDomain[] = [ + "clientes", + "estado-cuenta", + "polizas", + "servicios", + "chequera", +]; + +function ReportesCatalog() { + const [catalog, setCatalog] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + getReportCatalog() + .then(setCatalog) + .catch((e) => setError(e?.message ?? "No se pudo cargar el catálogo.")); + }, []); + + const grouped = new Map(); + if (catalog) { + for (const r of catalog.items) { + const list = grouped.get(r.domain) ?? []; + list.push(r); + grouped.set(r.domain, list); + } + } + + return ( + <> +
+

Reportes

+

Catálogo de reportes

+

+ {catalog + ? `${catalog.items.length} reportes disponibles. Cada uno corre como consulta sobre el esquema actual — los filtros y totales se recalculan en vivo.` + : "Cargando…"} +

+
+ + {error &&
{error}
} + +
+ {DOMAIN_ORDER.filter((d) => grouped.has(d)).map((d) => ( +
+

{DOMAIN_LABEL[d]}

+
    + {(grouped.get(d) ?? []).map((r) => ( +
  • + +
    {r.title}
    +
    {r.description}
    +
    + {r.legacyName && ( + + Legacy: {r.legacyName} + + )} + + {r.format === "statement" ? "Estado de cuenta" : "Tabular"} + +
    + +
  • + ))} +
+
+ ))} +
+ + ); +} diff --git a/apps/web/src/app/servicios/page.tsx b/apps/web/src/app/servicios/page.tsx index f47f08a..99ce739 100644 --- a/apps/web/src/app/servicios/page.tsx +++ b/apps/web/src/app/servicios/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { useCan } from "@/lib/abilities"; import { EXPIRY_WINDOW_DAYS, @@ -169,6 +170,13 @@ function ServiciosBrowser() {

Propiedades

+ {canCreate && ( + Nueva propiedad )} diff --git a/apps/web/src/components/AppShell.tsx b/apps/web/src/components/AppShell.tsx index 5ff97bf..846bc4c 100644 --- a/apps/web/src/components/AppShell.tsx +++ b/apps/web/src/components/AppShell.tsx @@ -14,12 +14,14 @@ import type { AuthUser, Ability } from "@/lib/types"; * content. Provides the AuthContext so any page can read the user's * abilities. Used by every authenticated page. */ -const NAV: { href: string; label: string; ability?: Ability }[] = [ +const NAV: { href: string; label: string; ability?: Ability; exact?: boolean }[] = [ + { href: "/inicio", label: "Inicio", exact: true }, { href: "/clientes", label: "Clientes" }, { href: "/servicios", label: "Propiedades" }, { href: "/polizas", label: "Pólizas" }, { href: "/estado-cuenta", label: "Estado de cuenta" }, { href: "/banco", label: "Chequera" }, + { href: "/reportes", label: "Reportes" }, { href: "/catalogos", label: "Catálogos", ability: "lookup:manage" }, { href: "/usuarios", label: "Usuarios", ability: "user:manage" }, { href: "/operaciones", label: "Operaciones", ability: "db:manage" }, @@ -78,7 +80,7 @@ export function AppShell({ children }: { children: ReactNode }) {
- + {NAV.filter((item) => !item.ability || can(user, item.ability)).map( (item) => { - const active = pathname?.startsWith(item.href) ?? false; + const active = item.exact + ? pathname === item.href + : pathname?.startsWith(item.href) ?? false; return ( ; + /** When true, opens the report in a new tab (for "see the catalog" + * style entries where the user is going to look around). */ + external?: boolean; +} + +export function ContextReports({ + label = "Reportes", + entries, +}: { + label?: string; + entries: ReportLink[]; +}) { + if (entries.length === 0) return null; + return ( +
+ {label} + {entries.map((e) => { + const qs = e.params + ? "?" + + new URLSearchParams( + Object.entries(e.params).filter(([, v]) => v != null && v !== ""), + ).toString() + : ""; + const href = `/reportes/${e.slug}${qs}`; + return ( + + {e.label} + + ); + })} +
+ ); +} diff --git a/apps/web/src/components/ReportRunner.tsx b/apps/web/src/components/ReportRunner.tsx new file mode 100644 index 0000000..b27808e --- /dev/null +++ b/apps/web/src/components/ReportRunner.tsx @@ -0,0 +1,476 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + API_ORIGIN, + reportDownloadUrl, + runReport, +} from "@/lib/api"; +import { CustomerPicker } from "@/components/CustomerPicker"; +import { formatMoney, formatNumber } from "@/lib/labels"; +import type { + ReportDef, + ReportParam, + ReportRunResult, +} from "@/lib/types"; + +/** + * The shared runner. Renders the filter form, fetches the result, and + * shows the table + output buttons. One component, every report — the + * per-report shape comes entirely from the def the API returns. + */ +export function ReportRunner({ + def, + initialParams, +}: { + def: ReportDef; + /** Pre-filled param values (e.g. when launched with a customerId from + * a context button on a customer detail page). */ + initialParams?: Record; +}) { + // The form state, keyed by param.key. Initialised from defaults + + // initialParams (initialParams wins for explicitly-set keys). + const [params, setParams] = useState>(() => { + const seed: Record = {}; + for (const p of def.params) { + if (p.kind === "select" || p.kind === "text" || p.kind === "number" || p.kind === "date") { + if (p.defaultValue != null) seed[p.key] = p.defaultValue; + } + } + if (initialParams) Object.assign(seed, initialParams); + return seed; + }); + + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const run = useCallback( + (p: Record) => { + setLoading(true); + setError(null); + runReport(def.slug, p) + .then(setResult) + .catch((e) => { + setError(e?.message ?? "No se pudo correr el reporte."); + setResult(null); + }) + .finally(() => setLoading(false)); + }, + [def.slug], + ); + + // Auto-run on mount so the runner opens with results, not blank. + useEffect(() => { + run(params); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function updateParam(key: string, value: string) { + setParams((prev) => ({ ...prev, [key]: value })); + } + + function applyFilters(e?: React.FormEvent) { + e?.preventDefault(); + run(params); + } + + return ( +
+
+ {def.params.map((p) => ( + updateParam(p.key, v)} + /> + ))} +
+ +
+ + + {error &&
{error}
} + + {result && ( + + )} +
+ ); +} + +/* ---------------------------------------------------------- one param field */ + +function ParamField({ + param, + value, + onChange, +}: { + param: ReportParam; + value: string; + onChange: (v: string) => void; +}) { + const label = ( + + {param.label} + {param.kind === "customer-picker" && !value && ( + + (requerido) + + )} + + ); + + if (param.kind === "select") { + return ( + + ); + } + if (param.kind === "date") { + return ( + + ); + } + if (param.kind === "number") { + return ( + + ); + } + if (param.kind === "customer-picker") { + return ( +
+ {label} + onChange(id)} + /> +
+ ); + } + return ( + + ); +} + +/* ---------------------------------------------------------- results block */ + +function ResultBlock({ + def, + result, + params, + loading, +}: { + def: ReportDef; + result: ReportRunResult; + params: Record; + loading: boolean; +}) { + return ( +
+
+
+ {result.subtitle &&

{result.subtitle}

} +

+ {formatNumber(result.rows.length)} fila + {result.rows.length === 1 ? "" : "s"} + {result.totals && ( + <> + {" "}·{" "} + {Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · ")} + + )} +

+
+ +
+ + {loading &&
Actualizando…
} + + {def.format === "statement" ? ( + + ) : ( + + )} +
+ ); +} + +/* ---------------------------------------------------------- tabular layout */ + +function TabularLayout({ result }: { result: ReportRunResult }) { + if (result.rows.length === 0) { + return ( +
+ No se encontraron filas con los filtros actuales. +
+ ); + } + return ( +
+ + + + {result.columns.map((c) => ( + + ))} + + + + {result.rows.map((r, i) => ( + + {result.columns.map((c) => { + const v = r[c.key]; + return ( + + ); + })} + + ))} + + {result.totals && ( + + + + + + )} +
+ {c.label} +
+ {formatCell(v, c.type)} +
+ {Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · ")} +
+
+ ); +} + +function formatCell(v: unknown, type: string): string { + if (v === null || v === undefined || v === "") return "—"; + if (type === "money") { + const n = Number(v); + return Number.isFinite(n) + ? new Intl.NumberFormat("es-MX", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(n) + : String(v); + } + if (type === "number") { + const n = Number(v); + return Number.isFinite(n) ? formatNumber(n) : String(v); + } + return String(v); +} + +/* --------------------------------------------------------- statement layout */ + +function StatementLayout({ result }: { result: ReportRunResult }) { + // The edo-cuenta-datos report synthesises a list with __kind + // discriminators (header, summary, movements-header, plain movement). + // Group by kind and render each block inline. + const header = result.rows.find((r) => r.__kind === "header") as + | Record + | undefined; + const summaries = result.rows.filter((r) => r.__kind === "summary"); + const movements = result.rows.filter( + (r) => r.__kind !== "header" && r.__kind !== "summary" && r.__kind !== "movements-header", + ); + + if (!header) { + return ( +
+ Selecciona un cliente y corre el reporte para ver el estado de cuenta. +
+ ); + } + + return ( +
+
+

{String(header.name ?? "—")}

+ {Boolean(header.address) && ( +

{String(header.address)}

+ )} + {Boolean(header.city) &&

{String(header.city)}

} + {Boolean(header.phone || header.email) && ( +

+ {String(header.phone ?? "")} + {header.phone && header.email ? " · " : ""} + {String(header.email ?? "")} +

+ )} +
+ + {summaries.length > 0 && ( +
+

Resumen por moneda

+ + + + + + + + + + + + {summaries.map((s, i) => ( + + + + + + + + ))} + +
MonedaCargosAbonosSaldoMovs.
{String(s.currency)} + {formatCell(s.charges, "money")} + + {formatCell(s.credits, "money")} + + {formatCell(s.balance, "money")} + {formatCell(s.count, "number")}
+
+ )} + + {movements.length > 0 && ( +
+

Movimientos

+
+ + + + + + + + + + + + {movements.map((m, i) => ( + + + + + + + + ))} + +
FechaConceptoReferenciaCargo / AbonoSaldo
{String(m.date ?? "")}{String(m.concept ?? "")}{String(m.reference ?? "")} + {formatCell(m.amount, "money")} + + {formatCell(m.balanceAfter, "money")} +
+
+
+ )} +
+ ); +} + +// Hint to the bundler that API_ORIGIN is part of the API surface used here. +void API_ORIGIN; diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index e529c68..1721ba0 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -44,6 +44,8 @@ import type { PropertyListResponse, PropertySort, PropertyStats, + ReportCatalog, + ReportRunResult, ServiceInput, TrustInput, Role, @@ -730,3 +732,37 @@ export function startOpsJob(kind: OpsJobKind, file?: string): Promise { body: JSON.stringify({ kind, file }), }); } + +/* ----------------------------------------------------------- Reports module */ + +export function getReportCatalog(): Promise { + return apiFetch("/reports"); +} + +export function runReport( + slug: string, + params: Record, +): Promise { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v != null && v !== "") qs.set(k, v); + } + const tail = qs.toString(); + return apiFetch(`/reports/${slug}${tail ? `?${tail}` : ""}`); +} + +/** Build a download URL for a report's file output. The session cookie + * travels with the browser's same-origin navigation, so a plain `href` + * is enough — no fetch-with-credentials dance. */ +export function reportDownloadUrl( + slug: string, + format: "csv" | "xlsx" | "pdf" | "print", + params: Record, +): string { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v != null && v !== "") qs.set(k, v); + } + const tail = qs.toString(); + return `${API_ORIGIN}/reports/${slug}/${format}${tail ? `?${tail}` : ""}`; +} diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 9f37040..3440ae5 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -1025,3 +1025,58 @@ export interface BankSummary { /** Cumulative figure the selected year opened on. */ opening: string; } + +/* ----------------------------------------------------------- Reports module */ + +export type ReportDomain = + | "clientes" + | "polizas" + | "servicios" + | "estado-cuenta" + | "chequera"; + +export type ReportFormat = "tabular" | "statement"; + +/** Param declaration a report exposes to its filter form. */ +export type ReportParam = + | { key: string; label: string; kind: "text"; placeholder?: string; defaultValue?: string } + | { key: string; label: string; kind: "number"; defaultValue?: string } + | { key: string; label: string; kind: "date"; endOfDay?: boolean; defaultValue?: string } + | { + key: string; + label: string; + kind: "select"; + options: { value: string; label: string }[]; + defaultValue?: string; + } + | { key: string; label: string; kind: "customer-picker" }; + +export interface ReportColumn { + key: string; + label: string; + type: "text" | "number" | "money" | "date"; + align?: "left" | "right"; + width?: number; +} + +export interface ReportDef { + slug: string; + title: string; + description: string; + domain: ReportDomain; + legacyName: string | null; + format: ReportFormat; + params: ReportParam[]; + columns: ReportColumn[]; +} + +export interface ReportRunResult { + columns: ReportColumn[]; + rows: Array>; + totals?: Record; + subtitle?: string; +} + +export interface ReportCatalog { + items: ReportDef[]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81e0574..7ffc27e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.4 + exceljs: + specifier: ^4.4.0 + version: 4.4.0 express-session: specifier: ^1.18.0 version: 1.19.0 @@ -49,6 +52,9 @@ importers: passport-local: specifier: ^1.0.0 version: 1.0.0 + pdfkit: + specifier: ^0.15.1 + version: 0.15.2 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -80,6 +86,9 @@ importers: '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 + '@types/pdfkit': + specifier: ^0.13.5 + version: 0.13.9 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) @@ -396,6 +405,12 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -717,6 +732,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.3.17': + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -799,6 +817,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -811,6 +832,9 @@ packages: '@types/passport@1.0.17': resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pdfkit@0.13.9': + resolution: {integrity: sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -975,6 +999,18 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -988,12 +1024,23 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1022,6 +1069,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1030,13 +1081,23 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + body-parser@1.20.4: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1054,6 +1115,12 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + browserslist@4.28.7: resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1066,12 +1133,23 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1107,6 +1185,9 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1174,6 +1255,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1199,6 +1284,10 @@ packages: resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} engines: {node: '>= 6'} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1243,6 +1332,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1255,9 +1353,15 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1283,6 +1387,10 @@ packages: babel-plugin-macros: optional: true + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1294,6 +1402,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1306,6 +1418,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1326,6 +1441,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1349,6 +1467,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -1364,6 +1485,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1415,6 +1539,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1439,6 +1567,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1477,6 +1609,13 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1496,6 +1635,9 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -1511,9 +1653,17 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1566,6 +1716,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1581,6 +1735,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1603,6 +1761,9 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1631,21 +1792,49 @@ packages: resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} engines: {node: '>=18'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1666,18 +1855,60 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1845,6 +2076,10 @@ packages: node-notifier: optional: true + jpeg-exif@1.1.4: + resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1884,10 +2119,17 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -1895,9 +2137,18 @@ packages: libphonenumber-js@1.13.9: resolution: {integrity: sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} @@ -1906,9 +2157,49 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -1989,6 +2280,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2100,6 +2395,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2142,6 +2449,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2198,6 +2511,9 @@ packages: pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + pdfkit@0.15.2: + resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2221,6 +2537,13 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + png-js@1.1.0: + resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -2234,6 +2557,9 @@ packages: engines: {node: '>=16.13'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2277,10 +2603,16 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2288,6 +2620,10 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -2325,6 +2661,14 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -2339,12 +2683,23 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2377,6 +2732,13 @@ packages: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2447,6 +2809,10 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -2463,6 +2829,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2527,6 +2896,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + terser-webpack-plugin@5.6.1: resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} @@ -2582,10 +2955,17 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2604,6 +2984,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2709,6 +3092,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2717,6 +3106,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2733,6 +3125,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2782,6 +3179,18 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2809,6 +3218,9 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2836,6 +3248,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + snapshots: '@angular-devkit/core@17.3.11(chokidar@3.6.0)': @@ -3233,6 +3649,25 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3670,6 +4105,10 @@ snapshots: '@swc/counter@0.1.3': {} + '@swc/helpers@0.3.17': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -3778,6 +4217,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/node@14.18.63': {} + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -3797,6 +4238,10 @@ snapshots: dependencies: '@types/express': 4.17.25 + '@types/pdfkit@0.13.9': + dependencies: + '@types/node': 20.19.43 + '@types/prop-types@15.7.15': {} '@types/qs@6.15.1': {} @@ -3991,6 +4436,42 @@ snapshots: append-field@1.0.0: {} + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + arg@4.1.3: {} argon2@0.41.1: @@ -4005,10 +4486,21 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} array-timsort@1.0.3: {} + async@3.2.6: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + babel-jest@29.7.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 @@ -4066,18 +4558,29 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.11.0: {} + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.4.7: {} + body-parser@1.20.4: dependencies: bytes: 3.1.2 @@ -4110,6 +4613,14 @@ snapshots: dependencies: fill-range: 7.1.1 + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.11.0 @@ -4126,13 +4637,19 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer-indexof-polyfill@1.0.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + buffers@0.1.1: {} + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4164,6 +4681,10 @@ snapshots: caniuse-lite@1.0.30001806: {} + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4227,6 +4748,8 @@ snapshots: clone@1.0.4: {} + clone@2.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -4249,6 +4772,13 @@ snapshots: has-own-prop: 2.0.0 repeat-string: 1.6.1 + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -4288,6 +4818,13 @@ snapshots: optionalDependencies: typescript: 5.7.2 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + create-jest@29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -4311,8 +4848,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + csstype@3.2.3: {} + dayjs@1.11.21: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -4323,6 +4864,27 @@ snapshots: dedent@1.7.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + deepmerge@4.3.1: {} defaults@1.0.4: @@ -4335,12 +4897,20 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + depd@2.0.0: {} destroy@1.2.0: {} detect-newline@3.1.0: {} + dfa@1.2.0: {} + diff-sequences@29.6.3: {} diff@4.0.4: {} @@ -4355,6 +4925,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -4369,6 +4943,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -4382,6 +4960,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.7.0: {} es-object-atoms@1.1.2: @@ -4415,6 +5005,18 @@ snapshots: events@3.3.0: {} + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.21 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.7 + unzipper: 0.10.14 + uuid: 8.3.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -4492,6 +5094,11 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4540,6 +5147,22 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + fontkit@1.9.0: + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4566,6 +5189,8 @@ snapshots: fresh@0.5.2: {} + fs-constants@1.0.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -4579,8 +5204,17 @@ snapshots: fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -4644,6 +5278,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-own-prop@2.0.0: {} @@ -4654,6 +5290,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -4676,6 +5316,8 @@ snapshots: ieee754@1.2.1: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4731,18 +5373,51 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + ipaddr.js@1.9.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.2: dependencies: hasown: 2.0.4 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4755,12 +5430,54 @@ snapshots: is-interactive@1.0.0: {} + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5127,6 +5844,8 @@ snapshots: - supports-color - ts-node + jpeg-exif@1.1.4: {} + js-tokens@4.0.0: {} js-yaml@3.15.0: @@ -5158,22 +5877,70 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + kleur@3.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} libphonenumber-js@1.13.9: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} + listenercount@1.0.1: {} + loader-runner@4.3.2: {} locate-path@5.0.0: dependencies: p-locate: 4.1.0 + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.groupby@4.6.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isundefined@3.0.1: {} + lodash.memoize@4.1.2: {} + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + lodash@4.17.21: {} lodash@4.18.1: {} @@ -5240,6 +6007,10 @@ snapshots: dependencies: brace-expansion: 1.1.16 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.2 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.2 @@ -5331,6 +6102,22 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5375,6 +6162,10 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@0.2.9: {} + + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5421,6 +6212,14 @@ snapshots: pause@0.0.1: {} + pdfkit@0.15.2: + dependencies: + crypto-js: 4.2.0 + fontkit: 1.9.0 + jpeg-exif: 1.1.4 + linebreak: 1.1.0 + png-js: 1.1.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5435,6 +6234,12 @@ snapshots: pluralize@8.0.0: {} + png-js@1.1.0: + dependencies: + browserify-zlib: 0.2.0 + + possible-typed-array-names@1.1.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.16 @@ -5453,6 +6258,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + process-nextick-args@2.0.1: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5494,18 +6301,41 @@ snapshots: dependencies: loose-envify: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 reflect-metadata@0.2.2: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + repeat-string@1.6.1: {} require-directory@2.1.1: {} @@ -5534,6 +6364,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restructure@2.0.1: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + run-async@2.4.1: {} run-async@3.0.0: {} @@ -5546,10 +6382,22 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5607,6 +6455,15 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -5675,6 +6532,11 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamsearch@1.1.0: {} string-length@4.0.2: @@ -5694,6 +6556,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5737,6 +6603,14 @@ snapshots: tapable@2.3.3: {} + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + terser-webpack-plugin@5.6.1(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -5760,10 +6634,14 @@ snapshots: through@2.3.8: {} + tiny-inflate@1.0.3: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 + tmp@0.2.7: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -5780,6 +6658,8 @@ snapshots: tr46@0.0.3: {} + traverse@0.3.9: {} + tree-kill@1.2.2: {} ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): @@ -5867,10 +6747,33 @@ snapshots: undici-types@6.21.0: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + universalify@2.0.1: {} unpipe@1.0.0: {} + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 @@ -5885,6 +6788,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -5959,6 +6864,31 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5990,6 +6920,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -6011,3 +6943,9 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2