From 7528468505f524f4386365dce08a843aaf3d8298 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Wed, 2 Oct 2019 16:33:53 -0600 Subject: [PATCH] Add client management --- action.php | 28 ++++++ database.mwb | Bin 15184 -> 15287 bytes langs/en/actions.json | 3 +- langs/en/clients.json | 7 ++ langs/en/labels.json | 4 + langs/en/messages.json | 4 +- langs/en/titles.json | 1 + langs/messages.php | 8 ++ lib/Client.lib.php | 223 +++++++++++++++++++++++++++++++++++++++-- lib/Clients.lib.php | 36 ++++--- pages.php | 18 +++- pages/clients.php | 94 +++++++++++++++++ pages/editclient.php | 50 +++++++++ static/js/clients.js | 30 ++++++ 14 files changed, 483 insertions(+), 23 deletions(-) create mode 100644 langs/en/clients.json create mode 100644 langs/en/labels.json create mode 100644 pages/clients.php create mode 100644 pages/editclient.php create mode 100644 static/js/clients.js diff --git a/action.php b/action.php index d1eba2d..8ea1cb3 100644 --- a/action.php +++ b/action.php @@ -106,6 +106,34 @@ switch ($VARS['action']) { ); returnToSender("event_added", $machine->getID()); + case "editclient": + $user = new User($_SESSION['uid']); + if (!$user->hasPermission("MACHINEMANAGER_EDIT")) { + returnToSender("no_permission"); + die(); + } + + if (!Clients::areLocal()) { + returnToSender("nonlocal_client"); + } + + if (Client::exists($VARS["id"])) { + $client = new Client($VARS["id"]); + } else { + $client = new Client(); + } + + $client->setName($VARS["name"]); + $client->setPhone($VARS["phone"]); + $client->setEmail($VARS["email"]); + $client->setBillingAddress($VARS["billingaddress"]); + $client->setMailingAddress($VARS["mailingaddress"]); + $client->setPublicNotes($VARS["publicnotes"]); + $client->setPrivateNotes($VARS["privatenotes"]); + + $client->save(); + + returnToSender("client_edited", $client->getID()); case "signout": session_destroy(); header('Location: index.php?logout=1'); diff --git a/database.mwb b/database.mwb index 52701720a0b53c405b453f79b15522707cdf564d..acd1974d6bf85aab8da65a213fd1aabf1cc6af9b 100644 GIT binary patch delta 14224 zcmbVzV~}P+(`DN|ZQFKF+qP}n_S3d)+qN-nOxwn^ZSBmv`^A2-yTA5ER94)$85K7w zqE4R7JT>Rm=8mNx4GM+|1Ox;HB;X??KfdH`M9~QBKC6D9NcgnkV^`V>;bMv6R-*O_Czy_WH(uXdjoPc%PTb zgPZ&DCjCgj^L28-@w!be(+4meNjrXi3wee%{}K_&1bmi|ZcAM5TdP+iFeA>Vc9Dd^ykG56(@7UQNvLzGNP%UR);5w9L9Jm+aJQ(x(6BGO0>j~4|j)jN0u zF7tnQaZ>)S$1OYcV3RzCQ`NsKS(tgzu_XEt=ZxN51lg`^-Is1Z?Vln?$QNjN8-9t(FhKE|T*5@!~IP1SAM zcgonJq4p^-rH0W4P z1P`PoZgRYRz5`r0Dm69R`(FPzot<2AT>_h<_lc zpkN*_%o6|^JfcmV@(*PJAp~l6(JpHgm9n@15i_VzU;ix~sNu;NJf|_5d@tIozn`yI z-&iy7=Ucd60@eT#C2pV;ut8>3(T*3kFxQt$#5owI^B)q-e0#zEWhqp*xLL~~$JxFBHbv7-QRIKp~(H+7GMxy5He%IBu$Y@ zlT;aTj&hC`BG2Gfd+^qfxR=`6AHttSKzlb(idG%Lh$zY+4y!C25Aa|PtKxsBc><1R zAb58u_*0H!6Bkmka3yR+7_O;kA=JVmMj`b4T>*vocK^U0k%9izD*tq%e1ic$bZ$h1 zdqB{6J}I3>Eg7v?`nrF9yz`#X3Ryf66Couj`EckHGEBq~Fe(VDTiBpxKYK7eDJn&) zw*aLU^2rD@DYSWF)S>I=eFSQT7)rLA;=y?Z^!ZH6o23lCpE<(!*nXho@+Y*birag- z87)3pc1j0`_lIE!w)S*fSwLPCsR{py7Jv!bc!D*V$SRy(h3HHVhng|MPj>`ao<1NK)%fOJ};;NPjox3K#2A5LgWkZy7B# zI46?8FZ1)XhHz6;$FBOOh9G0+1S1A!VvsYb>Y z%de;N<>Tut&*@|Mb%i!J{-VHmx{}OWBqB}|`P=(faZiAms>X|H>EX;mv?D`i)*g(V zJz@lU$`#=^DFa=n$znuXT9XReHsB>OT9i~?ASAjHi*Du!C>Fh9JamJv%3PvW%%Y zy6xK^->`aOK@KZ(ZxRs&0C?ud(4Z)31yi>H)!4<7e0dnfW76;rBa~RmXw>?Xwbq^u zGlSZ$lG=&wde#(>hWL+nuG<)sqhv7@$03!#l^-u^z+K`S(QzamQpz*888v| zCMs;poE;L04;Mi^Iab#+cTUp{5*B=^I!I?0K(0w6MZb;1+6%wP0U+p6awD!xu*D{M z{BQm4J0f>`ICiL|Yg+E5Tb0#NItwn;uFter0pXqmm{{9LPOp=X7e1yy@LeDH=lKDXyh~bcItQHn%M|O+Hi<++>6$+{_ zd9(hOEk2jTM!d5EfSyaD69}p%t3!U$qW_?=Fx*~44_q9)ECQFLU~jQXuL)O%)p#mu zA&KWZnGtBWYhZX(A!0@RbS}6R5ucfib#R0x%*6A(RPQ zvf?Ef81v{U0+>*&Nr-=Tm2Hv!DJsyJ@64=nZ3103Wlq}niw8;#IFm|>e z5f>=Pz#Mt)DGS2H2s|X1ZZJI10Dd5$eml^*r9V3*2BL6j@O1@qtp=oVQD5xS5Gbo6_hTv2zuG6s0#UdW7&+Mr9FmuhE zIm8nl0#LmkkO2YkT>WQ~FAO;V`HdRs}<^(&U z!%^iSKU5bdn(h=BB70W)a@z%id4^n*&!PK%AxSqRIgOHo)}0)PkRG{B?~*bB7LBxE zRL>clW}b;?R+p-E-X|F#ujTFOdv&Y?>M?BRUQS*wvNEMq8B|(Hpv8NeooBPmQBm=i zHO{hTH}{_wgJtLfEv+|`8Mwt=wsP_BBCrTsPcL7cfb(9yXsIM;TNiD#LP^~>m2lWc z3$NQuQwmu3hVRPa+Kxc6FIYSQcb#&GdV;n&hEi_mRxK%4fSxMdX4i7GvFk0k#ZGzw zTb6gYe7CUtW#KN+mNU8>AS>_!H@&-+Irv7S;1oHs)R)a_NZ!P(x6V(iu@j>}@74Ia z3~P(A=TP`pku&jY7h%2x_{!CIdD%)F(8xfQz8saRF1VpID+?CroW26UecRj_bz7QC z!}Ybdx1{aW4I?*z8`XH0@$J}xu`VuFjCW<%!oTSE04d69zCmJuWb1iO-#G-)+*8-D zx)AzkgS5+LU2^(1ocb9N5IMJ5E*9Osuw_k_6?6@Adk=$B+?iI>a~WP@xay{!)){*G z*_rRvPRC{JcNd&6{^w5s;j~V;I7pCf89duh&Fx?$3dD(KZ?Wv7bYIr}vyRyQ@E~n@ zM-n;CWyEL>#(lS`aPR0E(EFI64B394^P*RB`jJj&^}KW0{}UY5|0g!8xcxV5>_kmO zEe-aoYtrZ{JCT!A8z$2MYQV=Ovfh30O|M_#-}01>_+MJaw43Ip7!Ch-bo_7NDANR; zu*B~v7}>oDq#m8fq*`#7V#>MWe?A51RSlt&LS8AR<-1LZo>1^56*NJY_>*i3szq4b zYKcL0y4n`?fO=R;nUpA1bw)d*B9To&68pK?E%$%Hqknh}u!vow2kX9@&XCflEEp}i zZp^t6y2Lmi;343;H{x)JeUne7sYOn%D?k&ez4TP25}*~<&O&?~ZeZdeKE^pX11xkc zqb|lsEBuQJ$z6gY?hENIt{&k3n>)B`HZ$Lu82Mch=Yd*d`*OA(^OL^?gmRnyU^4WgcP2%95@5*QdOH9;I-m}HwCY149u>od0ssP%>B03 zijF-@Nw-33kwsm4w1H(2qRjt?f&SanpCbHc&yb&R!9ZVbWgQx?VRuRRh^Ph4-i%P- zu8$atN3kEjV9&nhWAgY=aVq1`;1=tEL|2#qUYPl-Q!T*2k@gt*<$5WulfaS0IDbhK zkpR*};{yrq@0K241o1_+5T1^JSnU1bIfgBze8y04Zpa z&B|osVw;3QYRLctoSP>91+!yE$S|S=-0y76!B2JxI4lpN@bwHF(og{-sDDO+8xSsF zG`5Q(%!4(+$exEepD-#*5n6rO2)Rad-Xy#%Q+SMuBxMulg&gDWok6_6p~s*kVu4At zW9FeP*a!hOeuj`pM(Izu#s@KuJQRwGB!P5lg}&veDA@#wr%60mOnN;aQtjU+hZ;e7 z=PO+ZMMxTX!3cqj!C0S$iHZ3+01$xlB<4=0m{$QT1OyEhLj(s!ilqO~ z?()^!V~+9H6X(`%SZpwAI2FbQ5UYf`U|Dp;j>dxmaS#H*!MDR*kO76R{)t0@0@48f zON8w{QC%`e_}hO-%@_WZ(m(|1Qoun(&7^Pnq|A4~=OU$q`@3$Q0%kB!2vEpYfdl#t zd;u)*Rq7d{FJa)3e{rAQXyCH+7^Gs zGywj*+a?)0YmcZeQMHGrvaRMopL4NzK-bCg!Eb1KwCF7SjGtwzPXcYW^*|dpTZik0 zpzpWleaDCJ*dp>Mv2fpAV)MBWVcUZC5FH9xArBB|X^m7=k8ZY$kEy>fj0YZwc)$1UwgB>{1OKS$NnwL2-dP{ zk?aQT5nxA`xqG1%JHkUX8P+Ip?Cm2MY@3TkW&;kOL$+yfZKDE;_3Kik!UipmGgc@V zh87t==oVbtWS(^~v`P`WOgpubahd@#VX3Ec%y?R*ZDn#HbIiWGbPIG;=Yu!w+ss@1 zLSIO&Kj7|jY;9^xyIdPD+z+u%Mz ze#3!n{dO;>(F@T%5|?G96ta5uM~oCWX8@Vbd39J2%?F$gBQ$v_FjsME{ZSvMoh`K^ zaP~Xz2u;+q$4o8Pll0i&>;u6;9YiLgMVhF8Ao=n8$7=5=k<~dZ(-bo;oH)Jc>Uo;v zF_;*Evd?+P(?2Ye;Tcgk2m<^AxV73eLreO^d?;yngJIC2YJyNr$bpb%LHsmaqjM z|EL2BV)BkjIJpEt8ZHnhq3%q1qRo;WlM#!HUT9lDSR7#k z1n!Fp0`r6f?mKje0An>c_)7Q!F=z2lL4fFnZ5^$$t`GvOcMF>+Kzkdr*nz~254Zvp5(!W*N#{m#!Y@1a1x zj(7E-NKDy3PSVebT%rE+PppB7G#FRFVZlL*{@(q1o&KoLfAA4?`g0tC%QQd!lk;mW zn1DIf!?pkm616r6Aay{2qW*w}0sTjnQ6Z27hy)DXDYLp24Vt;e01zHUoH#?#YPNlaJg2uM4ra5Y|>7KvUu9&VTyC-HS&)YBlm;_8W z$J?lSeEbFg5zR&;ln`JWksWM{Z3}T`oM)aTUmB8q@1%?y8wCsk*z+k9+4&N>L*rKfRLYj*A6aoQA02 z^I@To!M0*$=jRi!eGx+-=y%QTx~e1zrF7)Co*A>N+|tigi7(b1vJjJ8xAG)k%x(ZO zBGXU*FiPJeY}fKAd<}i@ zco`8KDP$KYnxjikSiZ$Izy^>cvV z;oA)cXf1fy*cAwsn1vLGG?pNA5%^z;kqcrI=m{AJ&qmm~BX@6yzBv_kpUK=nWUyW( z;*4@NX4`*vt{x87I{?9f13=0Fx64(Z=O6*^FYG@wu$tL)zoERa8h>s;WBDqx^b zIM-6s-wgD(NZ2K8fd0M9C4mC*gz!bGESnU}G7f&*`K3vJ(!BOv2GC7@_Ocqpb}muF zPHNN|oQk<2 z(5SFE+q&mkmn%@|7ATRHd6m(qMA|5nD?p=B7sxtQA#Y}^C({?b>t!h|HR5|@9a0*! zPBLEo1^*C50@+3{K&>p}ct0r$`#2aIWx9z$;-y#Cp=SV+MdBu5I%ujUKW!6zBCP6A zfmQo)JR83}|Ag3Xs-wN`SaEfbY;Z!k1%&Ut+jM23o=j zANF2&O5&m|9u8-3iDMZjGx3lj>?6nb0KPjq4S!u$UhSFm>88n1d5u9#rj~N;7Zyog zdu->vwQyAP$784iMoT)y#~gR;GfJWZ4qXzmwXc+-9#QRV5;9h4C!dI+yKi|u?9pSs z0{!Ybr}o_epfOJkiZjwh#8J2D0&lC(#WDX6O3L-9uUz^&EQc2Ked{^#l6cFUYl#IXjB97im5!7_wgzD zeXh`cA;qnQ^KOx>?A!~F$Hp*}(^o@X^%C>J1E3p+VtH{UqksD!R`W?zvj5Ke_W3aH z;2AI0X5{VPAbgMcgb1K-{PXKN!?})|kV>#e}s3r^ab1UZw~_9YL_Ad8QTIMb7e>9NM79t z)|nWcrx<&D!gmsmenxOo=Xtq2UwhBG2VBkO+yb}qwIv~5Uy%$e=l%Gx&so3$Qs+fM zxV^(O9LBdsJ{&fDfi|2pAn>u2X3vh1#!NW7Y|?BE^m@-fBj5Lk0QTt%HZaEy0T!sq zMYA7_*OV7M0|UWPjsnR|wopE@-n__{_f456aHm4VD~I3yQX#s+t29Ybe6d{~1;EAV z^I-ceE>BQGh<{3g9-~|sg}xGKEL%M2@@v{scNjB;rpBk&9v*M$YPRU~PO>bX+ivI;)qQ2qfc z9BmxugRq3w9Im{M#!J;L1F^K$te7Z`6A?AAmRYl*(itjvBVA#$aht+P6vGgu%(w!L zx-;#WMvUGKn>~t(;FJ^ANtT|YB!%8Tmag+I-47bG_l}56D0pss*cwOK$N;UWF^y`b z6|1(UY5ls5O4sN6*Dj0+x!g@1ov-`-yW_s-Z#vIkcYBj*@-UDzT(c^N2lCy&mf)jIZYUQJX%ocB< zmb?1vzdi?L4A=H1q7^>NfOTWdJ2wTv8lD1Q*w2TfjWa|VBFnoc<;?aJBmd9lO}|Ey zgjjx?iyDr-EtIm>tbX?RGHW|dUuy6PMMTQ(HW1URT!EHt@V)>MUq1e>m!}R?Dt*D? zsUEZnJl2sm3YjIvfC57CgJhnI++uHf=BDqbzAb(Lgc$MfFJi3(hw5oA5SNb>>&XFz~kP&lWWobCt@Qrt&O^< zjjGK;R>bP`D*<~XvD@3qvv>7~KqDK8<~x;Q5(nI9wUnbj3;3luOdi9`3}22j@;jBz zxXoZ=`ruZ>SJ%NLz&>xbj$ZCrx%TBQK;|@!O3s-7=(OQ-_DXW85;ch(v0{EI6(NH< z&kx|$GTiStRM`^ENg?vz=R!fhJQ=sia{0Z281%0Nul#yUlyZM0Rk{lpf8@ zoRJn<-p3mUDA%vMUjLK&HrtqfttiEP&TnHBNw(390m9-d2Q*O-*+saB481j0@sUMBJ73!aN~i;Wg9M++@#j zgosW788_KK;frZngGcIbf6y8B?KCXO%$TrZorWemWQl*8`QsL##NO7?{bf13G;^!M~L3VV3GdApPA9;u$L?99i}4|aMG zp(1!d?+0fzn_3Li1@!p-g4SpFJqPAJ6Be>pLCbDJ_yJ{%+VCUoenCy4>cTB2J5U)= zS!q?=e=*X7KFJgIM!S-pxjWxGU6M)ZN7=$I5MF zI0vgX2kPR~n4JDi7&%2F@5Ly%{tCK16_EtMwRUa`Z#IZIjDVH9K;|&^&nZoiXeb8xVGw`BA(p6Ntr!?dv4JXamZ8u5f>VG|uu9XZa1sXdVQ9925~%nT zszB_}!H!mtB7edz;S%!-p)J$gEOl2K4P#t-i8ntdP}OH03E526%{)G=QosSW1n6yN zkTcSXSvpX5@3P{}l1uu1IZGwhM|UxG+FeszwhNRrC59(4mdb>Hi04Ibc};!>)`JrZvwvDo@*}G>hm*({$V`* z5ANFa+lJ8r;411D`NKUX8+RRFKyFosLYXm#8Pdrb7u$xX9-r<(vR@kVZ)iy~6Cb?q zva|3--`Xgb2?B1nkqkYL7rq}++ySGIkP^<2`|z7(>X|x+Y>2F{=&YVijQQhX1>-r! zeQ}xD_cmiRPTsSqugbWqY$_cR4 zPycqwr6c0`a?A(Uo!3y4K)2&zV(cN_zNqY5v-L=;){jE&|E$qxx#3>2sl|UI}Zv}uQfNo{FN09&X5AWC3xN`E&r$X zxtKdk+Wo8fisK6f3B^OSV6WEbq9XFvUXv?}O^Jt^zi-~m9un`IHg}mddC3{|bU4Qc z-A4=pGT;JrSG=z7ZD}rL7i`8(; za%0nLkjT1hx{L$gz3lRzIE33t*k~E^SUcf+bC#1zo~y9YQRG+SRUP5KWYp4}KYX%u zmjpR<({`+@FF2E}DUdpfNav_7N2V#=Ee{|!Jp!=P-Hfj-<%RvlRKu$N%&q}}k=t*d zSLhUgRysyVe-COOgGmKXLWu{N)@sk(3&qdzYqAGqV>MhSc%@s&5V2I+mLKL1#EwFa z^(&L?#mn`!qr~#m>P&4|h-5G@oSa+yhmqUijVoJ&a^J|akBX>d8wm91EKcK^MF zLA9bAc#%5`);86zTtg3H6q26Ke|i_U5+KO321{(FG2Ml|UuTC^rYZPK zw%T7pEZzZmaH7p)jKZwGlxR`%HGdeVGJij5=g@P`Lrj?dipUI&wzhl;( z~?tIx*_~bqwiy^XyVnM9dF*wdRLxMGHDy@PIJ7R0^?$XL`U24iQY z!-e2j1)~;CJ@=8ADIlb>p=h@D#;oAQy}wxVEwb=6r|qaO;Knmn^kczeuQXQ=j7 zZKk5=w3H3#LJ*asVcHM^tm-uJ(Ik{h7t0$Uu6vDsJ*RKw{ne}N9})G?Kq@Lx2Eom= zyK(ajjNQb+xdh(?81}JDySMH%EZYh;BGG(h9*>A>OqimkIf6|V!WTp7J4;&#(PO}p zwr+Ju1{>5^OK;QZ4Bhao6fRvkov?3#t(}u3g$({IZDth%8)Cho6`HFrWrUw^IpJk}28vf+g>Ok$sJvAI~6b(yty)V=5kRfQ=bNb;in286@kiLpoj6_>=tJO1sH zZupq5cXqJGbBU2yoXqd)+K-Ze4$)T*IjXlbv19NAF1{!-LXM4@qk zFaYQ5C6Z6%K2zKF)x?0{nq>Yee2r7O#8iVz6{XcE)%g<*sA_~(hZD&U<&G*8Xh|D1 zPxJ*`G&5E7hNr}dYt-{9o7d!1*5$t3@pwPpGf0QoV)NZC({7Ml9$ixK@#}nFE@<9x z(pevLzsESvbnJWTqZU`EId3gq1C~+?QCb$FZvhMwPTi5oGmS2peOKu75x+zSZ5xDA zq~L|%`K62jZ>cPzFn_g*gC!cN36l>8*8~d}j(VgKH;yK+-v@!uKcfJ{#%xA+%~Hz8 z=jjS=X|Y2}pkqjH%n>-&1)ESxq_usc3vqqwjWoQuk-wC)6v3(g%29X!XlZ(uL@JUM z4^GN1ygI{2m}5V2qG_FmXxpK0JGqrM?YF{BYgL8-LfWx^zVEdX;nG-Si)$IGJ=!T1 zBB^UjD;@ACjnhaj4=VVjZSQPXRm6xJu~>qq7Ps-QeqO5hmX-OG%~Jf?VPUrO`B4Eq z+u^XoW*cDNH)+2bIYX<@Ty8qIZKNN6`tU+q(gfX>K|kR?2tI_LFca#Fh`1sg@VwgT zK_t=uIEpn<+f0$guIuBEmUMGaL>;e6?`@roER{YW>*9auK7=bf;%IFqypmw1-Z1W= ztTlEmQmP1XxZ39uU*#0|lXDubY-!2S@m67(x~vN^CObmZSI3o<`6Qi=&a>e zLwj$H>#2GzQ~OthKAv4qiYX1zwf=axZc|}70Clmj`#tAq3%=s~t(IFqMsz~D%%K1W z>sdg|@s5ZtvXdsmlrzeA`Gvc|7-w~;Hvm4GkuAYAOYIf@wN$%R%X&$$PVce8%J~nj zGj`OsGwtL^uCdY3jqex5A@NtzsaNd%1Nq{qth(#TuQrb**$53BkKZhs46{;+p4M%a zfJ`*3h^Sf+{V4n$I`)q~Pt^3+=a3nz=K;q@Z^`p?4Tg_)w59}QB{uHnIBs~G4#DQf znvI!*gHe6JUS(SL$LTlde@VT8fPT6P$=l4>dO-XYe*YU_e}|2|u{9G5DU``l^dFF_?)GhqR6MzQpqOvVdk_|YcgD0A3U2|G*MS>B&UQmDq6Y(!zmq$LA{ za&t0T(8)Wdu9RB6q85rn5Nz#vkJhz+>%laKJ*nl3CRi)x3nKIsgGYk+9?#)(H6_yi zjxMH($2GbPJBsAK{~YqX`kwBqDp|P>rV0~8ZIgh3w-um4ZLR|dm0PYSzC>GfrK98p z@UDPgI?!4wGkm?Izc536QPznM0Q&m|cKY@E#kHY1oep239ov4;>w`6JYN+ z5X%aTH0e!lv}JMse)phf)Wqy8Um8h6msG;1=pMI(sp}_<)nfFlqSEknKHFbp`}@Ogl& z@17f=6W)~U??m;{`)Pbg-g1vT?pu#ok3@w4(EU_jEce1fobKBx; z?_Ul@9hqWlxc5FES#5R*dI@W14V`0~ z4J|IdI|8r}pP)Ob4W+ZZV9OJT-GXv((qye2TiVh;ckU*GeY3a?ENgc5Bi{HBXMELnp~4*#zcUgBDSUDuHK6pIwW zP7Yc;h@?iIlkT;wvDu1AvL9nWKr}0ddOF|IX9K#il4iUllB!2i?j+P0I|Y?~w`7@1 zui2k#ZtNI* zZJlRLd_E@n=T^76LfoyWZtq(%4wMHDmImimk0wLh<4T<1yBpeDS2fD*zp0jSnieLT z96aBC}bK)TLvzMP5_TNGhpHR$$bc?#=B2g@HW2eKaCPqHt~gWi?hNh(xn)JyxjtAK~2 zK=f8DKr`Mk$rkHG?Mf@RGrTPz0Raf2@ulynq0|>Hv>Dp=BYbM$8A8y_+#A^xWx@px z@)W<0EB8trY2PlzV51tMblN4!VXK7}ZAM`BqNmKr2|;dKy9<(CLS{wM2W`74KJ2o; z5EAF|&NoM4)XmnkHz58pJ(W7o2r%SItKGpDxMwg*lqiq(5@2X_Y^(y$=q&Z8}8(dlvZj%pkvzr6-JhiT53R`!`+mI@a9m3Jj-@coUZ`#OdD;cL2s?vXr3a= zRv$s;jAqh69D{nCQP;HhoU^q?uQ!azT+l!Hb=XmM|E1EWa6@H<_vj5}v>sHbojOl* zaPXkEa>}gr0qy`R_gPYL}83LLJ06 zoON?Iz1hGSZH)NzAj(>eCHO<{^aIxhZN}hwSdHm4Yqj-z;OQ2#HE?kO4>;8=%mhD z=x5|mPdy4aU8P@1X@dq{;+=H?QSIGRn1uDAE*c03gmdEo#R|Qqx($Kj>s5zv7Nff3 zS?idI6{F}}QszXPiP#SKyHz+AOmM(3BD1&_%@k9yoWH^)u`cR|UJCbwy>VvBEe$vL z9n>luqP)BB9I)+{jRKyb&M4esyZ1WZCu0c*H!pyO8%TJ#h=t+a=E2#!+Be+& zTbhzBjP}e;v#DnnEDck=`q?6Lw3!O?{T3MV&)fB>@KZ>f$J?0iBzv0MkZ*$R^KUxH zt{=<@HVH6gn;VMMMeU}yPc)+?N;!B*-t<$uK30`1`L7ae!9}$iO{Zd zgncf-_{Tmp12IrWP83Z0OvlnM#+SlZShrMN?Vh~n-ajTHO$T4~>6wJ+tr9nx%uL|o2X^Ju(_ zZ)YCB?AQ0O-Zue9?YNKhOES=&uTL<{iB8@3-+e*2KpA56a0vg6&(|h{hcn@W=NTHI z`?`4FO%xIGD$Tl9YbXE`ti delta 14163 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 IGq3pn0T~D&PXGV_ diff --git a/langs/en/actions.json b/langs/en/actions.json index 11d9668..0fb4def 100644 --- a/langs/en/actions.json +++ b/langs/en/actions.json @@ -4,5 +4,6 @@ "View Schedule": "View Schedule", "View Machines": "View Machines", "Add Event": "Add Event", - "Add Component": "Add Component" + "Add Component": "Add Component", + "Add Client": "Add Client" } diff --git a/langs/en/clients.json b/langs/en/clients.json new file mode 100644 index 0000000..e8ea170 --- /dev/null +++ b/langs/en/clients.json @@ -0,0 +1,7 @@ +{ + "Name": "Name", + "Phone": "Phone", + "Email": "Email", + "Billing Address": "Billing Address", + "Mailing Address": "Mailing Address" +} \ No newline at end of file diff --git a/langs/en/labels.json b/langs/en/labels.json new file mode 100644 index 0000000..33e0142 --- /dev/null +++ b/langs/en/labels.json @@ -0,0 +1,4 @@ +{ + "Public Notes": "Public Notes", + "Private Notes": "Private Notes" +} \ No newline at end of file diff --git a/langs/en/messages.json b/langs/en/messages.json index 28d14e2..58b866d 100644 --- a/langs/en/messages.json +++ b/langs/en/messages.json @@ -1,5 +1,7 @@ { "Machine saved!": "Machine saved!", "Component saved!": "Component saved!", - "Event logged!": "Event logged!" + "Event logged!": "Event logged!", + "Client saved!": "Client saved!", + "Client must be edited in Invoice Ninja.": "Client must be edited in Invoice Ninja." } diff --git a/langs/en/titles.json b/langs/en/titles.json index 6649a77..aa8e1b2 100644 --- a/langs/en/titles.json +++ b/langs/en/titles.json @@ -2,5 +2,6 @@ "Home": "Home", "Form": "Form", "Machines": "Machines", + "Clients": "Clients", "Machine Info": "Machine Info" } diff --git a/langs/messages.php b/langs/messages.php index 3e11270..888045a 100644 --- a/langs/messages.php +++ b/langs/messages.php @@ -29,6 +29,14 @@ define("MESSAGES", [ "string" => "Event logged!", "type" => "success" ], + "client_edited" => [ + "string" => "Client saved!", + "type" => "success" + ], + "nonlocal_client" => [ + "string" => "Client must be edited in Invoice Ninja.", + "type" => "danger" + ], "404_error" => [ "string" => "page not found", "type" => "info" diff --git a/lib/Client.lib.php b/lib/Client.lib.php index 184ac7a..f8a4ffb 100644 --- a/lib/Client.lib.php +++ b/lib/Client.lib.php @@ -7,22 +7,231 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use InvoiceNinja\Config as NinjaConfig; +use InvoiceNinja\Models\Client as NinjaClient; class Client { - public $id = null; - public $name = null; + private $local = true; + private $exists = false; + private $full = true; + private $id = ""; + private $name = ""; + private $phone = ""; + private $email = ""; + private $billingaddress = ""; + private $mailingaddress = ""; + private $publicnotes = ""; + private $privatenotes = ""; - public function __construct($id, $name) { - $this->id = $id; - $this->name = $name; + /** + * + * @param type $id + * @param type $local + * @param type $full + * @param type $name + */ + public function __construct($id = "", $local = true, $name = "") { + global $database; + if (!empty($id)) { + $this->id = $id; + $this->local = $local; + $this->exists = true; + if ($local) { + $this->full = true; + $data = $database->get("clients", ['name', 'phone', 'email', 'billingaddress', 'mailingaddress', 'publicnotes', 'privatenotes'], ["clientid" => $id]); + $this->setName($data['name'] . ""); + $this->setPhone($data['phone'] . ""); + $this->setEmail($data['email'] . ""); + $this->setBillingAddress($data['billingaddress'] . ""); + $this->setMailingAddress($data['mailingaddress'] . ""); + $this->setPublicNotes($data['publicnotes'] . ""); + $this->setPrivateNotes($data['privatenotes'] . ""); + } else { + $this->full = false; + $this->name = $name; + } + } + } + + /** + * Fill in all the client data from the InvoiceNinja API. + */ + private function fullerize() { + global $SETTINGS; + if (!$this->local && !$this->full && $this->exists) { + try { + $client = NinjaClient::find($this->id); + $this->full = true; + // Name + $this->setName($client->display_name); + // Phone + if (!empty($client->work_phone)) { + $this->setPhone($client->work_phone); + } else if (!empty($client->contacts[0]->phone)) { + $this->setPhone($client->contacts[0]->phone); + } + if (!empty($client->contacts[0]->email)) { + $this->setEmail($client->contacts[0]->email); + } + $billingaddress = [ + $client->address1, + $client->address2, + implode(" ", array_filter([$client->city, $client->state, $client->postal_code])) + ]; + $this->setBillingAddress(implode("\n", array_filter($billingaddress))); + + $mailingaddress = [ + $client->shipping_address1, + $client->shipping_address2, + implode(" ", array_filter([$client->shipping_city, $client->shipping_state, $client->shipping_postal_code])) + ]; + $this->setMailingAddress(implode("\n", array_filter($mailingaddress))); + + $this->setPublicNotes($client->public_notes); + $this->setPrivateNotes($client->private_notes); + } catch (Exception $ex) { + if ($SETTINGS['debug']) { + echo $ex->getTraceAsString(); + } + sendError("Unable to query client from InvoiceNinja server:\n" . $ex->getMessage()); + } + } + } + + public static function exists($id, $local = true): bool { + global $database, $SETTINGS; + if ($local) { + return $database->has('clients', ['clientid' => $id]); + } + try { + $client = NinjaClient::find($id); + return true; + } catch (Exception $ex) { + if ($ex->getMessage() == "{\n \"message\": \"record does not exist\"\n}") { + return false; + } + if ($SETTINGS['debug']) { + echo $ex->getTraceAsString(); + } + sendError("Unable to query client ID from InvoiceNinja server:\n" . $ex->getMessage()); + } + } + + /** + * Save the client. + */ + public function save() { + global $database; + if ($this->isLocal()) { + $data = [ + "name" => $this->getName(), + "phone" => $this->getPhone(), + "email" => $this->getEmail(), + "billingaddress" => $this->getBillingAddress(), + "mailingaddress" => $this->getMailingAddress(), + "publicnotes" => $this->getPublicNotes(), + "privatenotes" => $this->getPrivateNotes() + ]; + if (!empty($this->id) && $database->has("clients", ["clientid" => $this->id])) { + $database->update("clients", $data, ["clientid" => $this->id]); + } else { + $database->insert("clients", $data); + $this->id = $database->id(); + } + return; + } + + if ($this->exists) { + $client = NinjaClient::find($id); + $client->name = $this->getName(); + } else { + $client = new NinjaClient($this->getEmail(), '', '', $this->getName()); + } + $client->work_phone = $this->getPhone(); + $client->public_notes = $this->getPublicNotes(); + $client->private_notes = $this->getPrivateNotes(); + $client->save(); + } + + public function isLocal(): bool { + return $this->local; } public function getID() { return $this->id; } - public function getName() { + public function getName(): string { + if (empty($this->name)) { + $this->fullerize(); + } return $this->name; } -} \ No newline at end of file + + public function getPhone(): string { + $this->fullerize(); + return $this->phone; + } + + public function getEmail(): string { + $this->fullerize(); + return $this->email; + } + + public function getBillingAddress(): string { + $this->fullerize(); + return $this->billingaddress; + } + + public function getMailingAddress(): string { + $this->fullerize(); + return $this->mailingaddress; + } + + public function getPublicNotes(): string { + $this->fullerize(); + return $this->publicnotes; + } + + public function getPrivateNotes(): string { + $this->fullerize(); + return $this->privatenotes; + } + + public function setName(string $name) { + $this->fullerize(); + $this->name = $name; + } + + public function setPhone(string $phone) { + $this->fullerize(); + $this->phone = $phone; + } + + public function setEmail(string $email) { + $this->fullerize(); + $this->email = $email; + } + + public function setBillingAddress(string $address) { + $this->fullerize(); + $this->billingaddress = $address; + } + + public function setMailingAddress(string $address) { + $this->fullerize(); + $this->mailingaddress = $address; + } + + public function setPublicNotes(string $notes) { + $this->fullerize(); + $this->publicnotes = $notes; + } + + public function setPrivateNotes(string $notes) { + $this->fullerize(); + $this->privatenotes = $notes; + } + +} diff --git a/lib/Clients.lib.php b/lib/Clients.lib.php index 1a51b89..2ccac8b 100644 --- a/lib/Clients.lib.php +++ b/lib/Clients.lib.php @@ -19,49 +19,59 @@ if (!empty($SETTINGS["apis"]["invoiceninja"]["token"])) { if ($SETTINGS['debug']) { echo $ex->getTraceAsString(); } - sendError("Unable to load client list from InvoiceNinja server:\n" . $ex->getMessage()); + sendError("Unable to load InvoiceNinja API:\n" . $ex->getMessage()); } class Clients { public static function getAll(): array { - $clients = NinjaClient::all(); + try { + $clients = NinjaClient::all(); + } catch (Exception $ex) { + if ($SETTINGS['debug']) { + echo $ex->getTraceAsString(); + } + sendError("Unable to get InvoiceNinja client list:\n" . $ex->getMessage()); + } $list = []; foreach ($clients as $client) { - $name = $client->name; - if (empty($name)) { - $name = $client->contacts[0]->first_name . " " . $client->contacts[0]->last_name; - } - $list[] = new Client($client->id, $name); + $list[] = new Client($client->id, false, $client->display_name); } return $list; } public static function getClient($id): Client { - $client = NinjaClient::find($id); - return new Client($client->id, $client->name); + return new Client($client->id, false); + } + + public static function areLocal(): bool { + return false; } } } else { + // Use internal client table class Clients { public static function getAll(): array { global $database; - $clients = $database->select("clients", ["clientid", "name"]); + $clients = $database->select("clients", ["clientid"]); $list = []; foreach ($clients as $client) { - $list[] = new Client($client['clientid'], $client['name']); + $list[] = new Client($client['clientid'], true); } return $list; } public static function getClient($id): Client { global $database; - $client = $database->get("clients", ["clientid", "name"], ["clientid" => $id]); - return new Client($client['clientid'], $client['name']); + return new Client($id, true); + } + + public static function areLocal(): bool { + return true; } } diff --git a/pages.php b/pages.php index a70e4ee..0632d94 100644 --- a/pages.php +++ b/pages.php @@ -25,6 +25,19 @@ define("PAGES", [ "static/js/machines.js" ] ], + "clients" => [ + "title" => "Clients", + "navbar" => (empty($SETTINGS['apis']['invoiceninja']['token']) ? true : false), + "icon" => "fas fa-users", + "styles" => [ + "static/css/datatables.min.css", + "static/css/tables.css" + ], + "scripts" => [ + "static/js/datatables.min.js", + "static/js/clients.js" + ] + ], "404" => [ "title" => "404 error" ], @@ -39,5 +52,8 @@ define("PAGES", [ ], "editcomponent" => [ "title" => "Edit Component" - ] + ], + "editclient" => [ + "title" => "Edit Client" + ], ]); \ No newline at end of file diff --git a/pages/clients.php b/pages/clients.php new file mode 100644 index 0000000..96df6c8 --- /dev/null +++ b/pages/clients.php @@ -0,0 +1,94 @@ +hasPermission("MACHINEMANAGER_VIEW")) { + header("Location: ./app.php?msg=no_permission"); + die(); +} + +$writeaccess = $user->hasPermission("MACHINEMANAGER_EDIT"); + +$clients = Clients::getAll(); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
get('Name'); ?> get('Phone'); ?> get('Email'); ?> get('Billing Address'); ?> get('Mailing Address'); ?> get('Public Notes'); ?> get('Private Notes'); ?>
+ isLocal() && $writeaccess) { + ?> + get("Edit"); ?> + getName()); ?> + + getName()); ?> + + + getPhone())) { ?> + getPhone())); ?>"> + getPhone()); ?> + + + + getEmail())) { ?> + + getEmail()); ?> + + + ", explode("\n", $c->getBillingAddress())); ?>", explode("\n", $c->getMailingAddress())); ?>getPublicNotes()); ?>getPrivateNotes()); ?>
get('Name'); ?> get('Phone'); ?> get('Email'); ?> get('Billing Address'); ?> get('Mailing Address'); ?> get('Public Notes'); ?> get('Private Notes'); ?>
\ No newline at end of file diff --git a/pages/editclient.php b/pages/editclient.php new file mode 100644 index 0000000..8bbf037 --- /dev/null +++ b/pages/editclient.php @@ -0,0 +1,50 @@ +hasPermission("MACHINEMANAGER_EDIT")) { + header("Location: ./app.php?msg=no_permission"); + die(); +} + +$editing = false; + +if (!empty($_GET['arg']) && Client::exists($_GET['arg'], Clients::areLocal())) { + $editing = true; + $client = new Client($_GET['arg'], Clients::areLocal()); +} else { + $client = new Client(); +} + +if ($editing) { + $form = new FormBuilder("Edit " . htmlspecialchars($client->getName()), "fas fa-user", "action.php", "POST"); +} else { + $form = new FormBuilder("Add Client", "fas fa-user", "action.php", "POST"); +} + +$form->setID("editclient"); + +$form->addHiddenInput("action", "editclient"); +$form->addHiddenInput("source", "editclient"); + +if ($editing) { + $form->addHiddenInput("id", $client->getID()); +} + +$form->addInput("name", $client->getName(), "text", true, null, null, "Name", "fas fa-user"); +$form->addInput("phone", $client->getPhone(), "tel", false, null, null, "Phone", "fas fa-phone"); +$form->addInput("email", $client->getEmail(), "email", false, null, null, "Email", "fas fa-envelope"); +$form->addInput("billingaddress", $client->getBillingAddress(), "textarea", false, null, null, "Billing Address", "fas fa-file-invoice", 6); +$form->addInput("mailingaddress", $client->getMailingAddress(), "textarea", false, null, null, "Mailing Address", "fas fa-mail-bulk", 6); +$form->addInput("privatenotes", $client->getPrivateNotes(), "textarea", false, null, null, "Private Notes", "fas fa-comment-dots", 6); +$form->addInput("publicnotes", $client->getPublicNotes(), "textarea", false, null, null, "Public Notes", "far fa-comment-dots", 6); + +$form->addButton("Save", "fas fa-save", null, "submit", "savebtn"); + +$form->generate(); diff --git a/static/js/clients.js b/static/js/clients.js new file mode 100644 index 0000000..4db2152 --- /dev/null +++ b/static/js/clients.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +$('#clienttable').DataTable({ + responsive: { + details: { + display: $.fn.dataTable.Responsive.display.modal({ + header: function (row) { + var data = row.data(); + return " " + data[2]; + } + }), + renderer: $.fn.dataTable.Responsive.renderer.tableAll({ + tableClass: 'table' + }), + type: "column" + } + }, + columnDefs: [ + { + targets: 0, + className: 'control', + orderable: false + } + ], + order: [ + [4, 'desc'] + ] +}); \ No newline at end of file