From 05dba7238920e128a8dd916f1264cd069f10aa95 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 Aug 2025 11:46:19 -0600 Subject: [PATCH] Fixed a bunch of the template stuff --- TEMPLATE_FIX_SUMMARY.md | 42 ++++++ data/campaign.db | Bin 110592 -> 114688 bytes docs/instruct.md | 104 ++++++++++++- src/__pycache__/app.cpython-311.pyc | Bin 97171 -> 103088 bytes src/app.py | 125 +++++++++++++++- src/static/js/dashboard.js | 217 +++++++++++++++++++++++++++- src/templates/dashboard.html | 144 +++++++++++++++++- test-templates.sh | 23 +++ 8 files changed, 641 insertions(+), 14 deletions(-) create mode 100644 TEMPLATE_FIX_SUMMARY.md create mode 100755 test-templates.sh diff --git a/TEMPLATE_FIX_SUMMARY.md b/TEMPLATE_FIX_SUMMARY.md new file mode 100644 index 0000000..a27ab98 --- /dev/null +++ b/TEMPLATE_FIX_SUMMARY.md @@ -0,0 +1,42 @@ +#!/bin/bash +echo "๐ŸŽฏ Template Loading Bug Fix Summary" +echo "==================================" +echo +echo "โœ… ISSUES IDENTIFIED AND FIXED:" +echo +echo "1. DUPLICATE loadTemplate FUNCTIONS" +echo " - Problem: Two loadTemplate functions in dashboard.js (lines 342 & 660)" +echo " - The second function overwrote the first, causing templates to not load" +echo " - Solution: Renamed the simple one to loadLegacyTemplate" +echo +echo "2. CONFLICTING EVENT LISTENERS" +echo " - Problem: Both Alpine.js @change and manual addEventListener on select" +echo " - This caused race conditions and multiple calls to different functions" +echo " - Solution: Removed manual event listener, kept only Alpine.js @change" +echo +echo "3. IMMEDIATE TEMPLATE CLEARING" +echo " - Problem: @input=\"selectedTemplate = ''\" cleared template immediately" +echo " - This could interfere with template loading process" +echo " - Solution: Added intelligent clearing that only clears when user modifies content" +echo +echo "4. DEBUG INTERFERENCE" +echo " - Problem: Debug functions and test code interfering with normal operation" +echo " - Solution: Removed debug functions and test event listeners" +echo +echo "โœ… CURRENT STATE:" +echo " - Only one loadTemplate function (async, handles database templates)" +echo " - Clean event handling via Alpine.js only" +echo " - Intelligent template clearing behavior" +echo " - Simplified, robust code" +echo +echo "๐Ÿงช TESTING STEPS:" +echo "1. Open http://localhost:5000" +echo "2. In Create Campaign section, click 'Use Saved Template' dropdown" +echo "3. Select any template (e.g., 'Volunteer Check-In')" +echo "4. Verify message template field is populated with template content" +echo "5. Verify 'Template loaded successfully' message appears" +echo "6. Test that manual editing clears the selected template" +echo +echo "๐Ÿ”ง FILES MODIFIED:" +echo " - src/static/js/dashboard.js (removed duplicate functions, cleaned up)" +echo " - src/templates/dashboard.html (removed conflicting event listeners)" diff --git a/data/campaign.db b/data/campaign.db index 1cf354c76038e28d3d42fd5412ca530889e1e69e..d4ac4533e62a9535ba71f658a08589c4442cb7d2 100644 GIT binary patch delta 2785 zcmcgu+iw(A7@yfK-CMh-w7XrQEj^V=CD85e%+B60(WNd00fA-_h$Lm$J?#!VJG0EC zrSY;I=(?tbgsoa5x$PTM1jqI&V$d^XN+W^A@c)g$MDz1LPS9UW||{d0TRR;U`A zY-MA#w~g`}=~OD!?{YaaNY%Cr@GXfiqIU-1Hdz$q89|zYdI`!BlywD`6f`Z!x(c(h zG>sIM*92J#!d*y%MFb0yJOhPEI4kQg&rc)BOSAB}j?H2zb$|&m+;Av9%y2LqOGdbI zk_`n}j%C^CldL2g4#vV!Hj-G$VS;g%jWJACE|x^3p*2 zNd(zYBEoS^PPrZkW?^s9NHol`P1X{J3C3ax9LJ)!*|i>u^*6kY^2HJ_Oi6G_kp!?r z!EiLmVFz50iHAdM$jWK3VnLQ+7%pOE)s^DGNFMa(Vnb+)6O=0v>PMy)j`w)CvVI9=R`P&^uEu}uF$A^JG)!T60P!%Y6-xtyt< zdCy8;I@<#2)#j?}>uv3a=8Y{2==v{in{BXaK->8Hu{Dms$O!#GpT-{*(TeR_G0{6Z zo=#=bFq7K4D~%7?6Wa@4G>_l#o1a}`KKa3EzW$@jeEV|1oVejMFMc;j{+=9Cvx-n6 zN6-u}bYZdnEC$Autjxkp`d|jWn%ck{JEh~bC%%5%! znjJS6T9!6~4CU0^e4^Zwzz zRW&YoJ?mUfvS&TGT$$(Y`915LO}eK}JDT)bLN9E-cjd^2M$T>BcGhJ*ERA_L!7%u+ z;&_FA-f;@dgAc*iRpT%KEq03bkxv)-bXGpyLko5vI9+kWX8ISltNv!0M{;z&q%% z_s|W%`Yhuv%eb?`eN5l6`@D}Uj*~?& zf1z$CgR4Gy8{HF<@k(o?oG{5q+~S})Wo%jHp@t1^XDzI;`cZ^?%DSc?KAX3sdb?ag zBt=6Cf;pMwYX*s85FX$q+zd$?YZ^G`ct@lhNh-|a#;oPBEY2WSn<8q#rV}h!*C5h3 z(-O*(YEcWn%!|u|{imcFTU$I-zmeYG=X9}b{q6wwmn?Drr~5?t-!2plFZJt*XLwq9JzH&)^0I+=^0nS**(;VF*Mdsz>&%GWvKuUVL0xt7hcP9Fcb3#R8<)k^=zI1 zna~Q%Be7IZy(^QgEo6oS@`x}o6 BCrSVS delta 376 zcmZo@U~hQ9HbF{=QHFtmK^ll*Kx3kg5fh`##)KvO%#tjUli3A~H#Xj9-YhBdnp=R4 ze>wyINB+nBSNIR|FXx}WSumiPUx1H|S&^|QH94~&Gc~WIm~HZMeNS;gUPfkl#^l7@ zg2c@9ykbdSpap_#S(Difls7gyux-vV`s^}r=ACYHF3jZrL$W+(BLf6Pt!4N9W1#$-e?CI o6j7PZJ^>{1i*Nehd5k>M4dydm0SbptXI}sk*5#ibzMgR<04DlsZ2$lO diff --git a/docs/instruct.md b/docs/instruct.md index 8c9a3e1..ab79f58 100644 --- a/docs/instruct.md +++ b/docs/instruct.md @@ -682,7 +682,109 @@ const list = await response.json(); } ``` -#### Analytics & Reporting +#### Templates Management + +**GET /api/templates** +```javascript +// Get all message templates +const response = await fetch('/api/templates'); +const templates = await response.json(); + +// Response format: +[ + { + "id": 1, + "name": "Volunteer Check-In", + "content": "Hi {name}! Hope all is well. Are you available this weekend?", + "description": "Check availability for volunteer events", + "category": "volunteer", + "is_favorite": 0, + "times_used": 0, + "created_at": "2025-08-25 16:39:21", + "updated_at": "2025-08-25 16:39:21" + } +] +``` + +**POST /api/templates** +```javascript +// Create new template +const templateData = { + name: "Follow-up Message", + content: "Hi {name}! Following up on our conversation...", + description: "Follow up template", + category: "followup", + is_favorite: 0 +}; + +const response = await fetch('/api/templates', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(templateData) +}); + +// Response: +{ + "success": true, + "template_id": 6, + "message": "Template created successfully" +} +``` + +**GET /api/templates/:id** +```javascript +// Get specific template +const response = await fetch('/api/templates/1'); +const data = await response.json(); + +// Response: +{ + "success": true, + "template": { + "id": 1, + "name": "Volunteer Check-In", + "template": "Hi {name}! Hope all is well. Are you available this weekend?", + "description": "Check availability for volunteer events", + "category": "volunteer", + "usage_count": 3 + } +} +``` + +**PUT /api/templates/:id** +```javascript +// Update template +const updateData = { + name: "Updated Template Name", + content: "Updated message content", + is_favorite: 1 +}; + +const response = await fetch('/api/templates/1', { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(updateData) +}); +``` + +**DELETE /api/templates/:id** +```javascript +// Delete template +const response = await fetch('/api/templates/1', { method: 'DELETE' }); +const result = await response.json(); + +// Response: +{ + "success": true, + "message": "Template deleted successfully" +} +``` + +**POST /api/templates/:id/use** +```javascript +// Mark template as used (increments usage_count) +await fetch('/api/templates/1/use', { method: 'POST' }); +``` **GET /api/analytics** ```javascript diff --git a/src/__pycache__/app.cpython-311.pyc b/src/__pycache__/app.cpython-311.pyc index 76db7ef42c722324cebec829b2af850f329dd3c0..caaba71bd317c8b553b5005482d807058a58129c 100644 GIT binary patch delta 9748 zcmbt34S18))$dL6CU4TTN&j1D;cZKQ(v~8E2*_WhKeW<{sN zCpkIy+;h)8@45G!bMJZCXZ*46>C?WSnrhTNOjQalZK>v_`g|d*v_q^(tfECT3+cu=wrd7&ED8W zp1SUF4aYqRzt~6WVbole>y%uV=<8+AseSHC^m*9d%~hn2oi%2Yo7v^5RfMXzm$?D= zskom#0eHWP53r9?tI{_;PGENU#b@8lYSOCG0~b)g%Qgaa%LUY}?5VUWvQ3p1WFG)N zh;5R+&Xr@IltuA3x?lz5!(a6Mz*UmZojB=FUq*%0vUH+#I_IE8t=M* z?e2@%6zLvVkRc5xr^!Q4?U=~FZof2N-rsS6t+N!7ovJ?fUL@flDtn*Wx$C0NL)f_+ zJD2UbsPjSTewh0qd5Oe&)ODvO(Bq+rHY1$$0B{|aG^;qmZ; zIE9nQR?bJ*$mf%UI&QaSy>BZ~-kDDFl`o1(N@$hi8^XSp^4NP%rn9f4{7lQlXC&Mf zExY#W92O3wiIO#ce6b{JT&YQ2oRt0u*{TD^%Xa5xXKc|;+^Qf#qx5MA%19>~w!0KD ztHw8lg-eUrypPh^TXJ@0dP4Lxj&9q^OIf~BW${chDX*WSja$eTPRb5|hxMpzbP;5g8uWGf#VKnj&`Dapw@&pE(t(yrwOh%^NZKQ*+b>kfm) zKASCe%`tcQ?$e!+=YKZr-*IlJ4lI(R*XP?jMakFsG z*2J38(C9_sYT%{Vx-*54&@0GU}iOW2FvS@d?M5k9AkX^1Cv6jl> zW{+RA_uHK=`v#XR`nuIlxknDVCDG>*yJc6OIN+kFHP+yJM zYWF#PehJpVgE|IPC8Nf1I;dl8@VH!_0daL- z;Kn31VpM!wf1l`at0n@W{Z5bHi=z5v>ILB*H&{wWEyNx-NO5+m=IOTg!?tj57H{^; zUgQF;rZsqi>P#_#Ny2)2{cb00bmWT)ovJ?mKG4nD7~AIJ3cruac1L%hJxN7%?}1^; zVaG`xR5i8(*Vtjhstcx$0o7sg(Q0x~7P~=7*pTinuo;Z&!f`c$sfmezH#P_dz+<{) z2dWh{#2qfMwyAYheOtTO)Y`t{V#g6H&@ZaRgtu3VN&Z|d7ACyeCSF&&e0BXQv0`zx z82{SXY4_(OkNw8enj>_|K1a95Ek{%2O|rue^1zy`q_3IinY*tr*8+Jrnl8zmcE8Ja z-f@*9T zBNj1Y5s#!tEG0*7e#24`wp5I1^rabNTmpoxMPpVjJ@>cP;y0|tqgkHOtkQG5Ha+7L z4#1cSOrhW1TtX@vW;dp4PNiC!dHz&EZAMcz|H|wlfUjn!0_oM_Da{=J8pi{!j1-fc z&;qZGG^fJj(j5@Gs2MRoVrgh7ZJTExnnFDT5GNfzh^96|bD_fvKTf1&0HgXYS+!Xq z&|VBZ&c^&5P!s8OStId~u$hmLPc=f~2X4@=!U+OE<;0 z4eo+UU^yFV*lN-7(*gY=wC2j zQN@`ED%f|FYO;vks~jO$v6`U(zXllAhJH8{6v&IA=?_mLnd^Y!#UY|NsM?gSXN8a2 z$Vs;SkvqP%_chk|=t8}W({-|Y9=-8f8Gp@69{VL90LGt&{`}axT0RKqo1xzv%{P+T z(AcvFHN?(#KKE$KMwHbDAaEZ`KY1D3`20ttg01=S+L~?HvlpLdIs%lXS|^&?Nj<$j zCnQ3jy|<6HU`H!E^W!?(?T8*m?)MSgp-KP{)%JS3=tiX7g|xfbHN*2wTMz7JX?m>nf2zG{QUY<%O z-HZ5r2zDVjj#BJ^1s-L4o@-`Dx^?XB=W5y6y%xhv2qiYUo0(4@((gfL#-2V|RPX?z zTL9Q5Q{01;q6~Fu751l-%d*GKK!>n<{NW4R7a?aK0GoyG$G!syu0Zf8P@}xd>6X28 zFA$1Hsq_%GA7no{Rculh0zpgYy;Hk*GM)KLBUHy`x?gYOpN7^iLU;c9MveX$p!8YxVYrfi9?+AaypeOH^hbb3b>7Wx2Sv2O zrlq*kDee$zM*TJPN37<}YW`PAbS$dq8>e-X0 z>+JIqV`F=z8Jn-8J=6e@ip@)e8j69LE&~uXy6j#b#2lxT=+tpF#`4B)BuCVqC*H&OZLm2R0ow9=m*G>YT{b zImdM`3O^N26oyx}jm%vYnY(HfK%kpX@k42?f0V~E=bI$c1cpDIXE4PX%*hPOF%8LJX9IaC!W`#FOXd)yc%h8_CQyuAX>l%V zG8fzaf|Z>KOhN{$%Ant(rPs#P6r_yUc0v*C?a!@j?)EgDlqqE?&ss>1vTG%=UJv!G z><6_Mm(+ZQ#O~xcDM!*=BJkt}@>Z+yT$DFK^1(ZEV&MbRS@=ge!SRjYz0^LWe}{bm z8xO!|h+&8c2x>rw&8o(kfH@%%_b(j-wtyw+7OYawA=R9SK`QC5Dq}Dy5Ft4*<$V5u z_R`gDZS}3~9qmmm^{d)zTUG`NSFfzAZLhz?ReV)_yGW0q)YJ)&qPmLBkQCX*lQeBs zPmCDpqPmSx8h`-!4`{^B{^(s%eWz1)N#3Zw-|m9ih8KJUQWbn+x5(_6y5m;th`AF7 z8SOoEr&mEnQnZAWWz7rcHNH~y%9QYWd)N}(Fk;ydv1}MzHky+8ul;FE&!t%h8%9$t zgAHd5sk^fVw+(I^71DO4-jlj}QGy~3&3V8zB9ugglCV%RYOwCE3mfv_ zn_zGUM}*RdP#P9W6C`P9-UFTyp)4Ylg@v+FL)MTkY$$+loFSZ3J|a{^go?0GaemCg zNRDkpsEi1eVWD!=kiNS;Y{-Xig2R1@dFDrQ%3%KTh)^CD%KuL@fepqu18*AFZO({r zWkk3#EL@qGxo1QujtIqJq4+%^W7NF-Xx%eSPc$9t9I0Lysa{wZHZPBTN6i^fn=qOn zC>c!DG==Ooa_C(&1 zRBf>K=y1Tk@ASJ|n-h7*2@f_Q@2F>m&tb1mNHp0o8OhEO#AKrd??{7e?Cl3R;2nbM z6iu&O^U9)dha+r}Ml4dqBK?~_@t!U%Y`N-q&+(f^bju^U-mrGG+nIf9=nMFOcx z#XDG(S09bOsH_!8t5v`;CInw0Fe}FeGAAPyri_~E;7Zjd+o5z%=kGI;JkqQ{VdqLR zPr24e#DZloz?Mx7z)N#*fY9l5$$fTTH(jaRYa~-zR55zExuA#Z`dkoyd z=|Y_G4Cog%s^pFyc{5#w#E+1uakyv?YM+HOyr_I`B(wEZfS2iqFE^3(#v+`$feP5I zCcqOoRy|%Dk!I2;$1+Kk#;Q?HXOaWKlhEC2%ZjC$Y~;XIP<5yHMv3MkZ4!W}UXmRi z(7FIAQ;}lw_Q7k9Saqcd=b4W{Em}1rhSs>?njWjum}3??Olnbz!`t5xH%JYNwW>vT zc>u0`vG*0U4F##jN^^T?uifPgB!s;lm>ND3H8h)*ue5B3AduXgc3`8z*6J4+TstOi3 z)1Z7&MQ$ahl&#ey&p3d*-vCe^t|s&KYhVO#7=EXkv}p9}ppj!3o;`yw&D<7foZmUo zRM`#hfgE6M$!~WhBI!Pq7E>>NLOG>;Gn0HrzaB4D`iJ+Z$UGBWVfOlU(%5odv zESP*>2C-1{Zct8ML8j>KAZxy1_>WhR?`t%$Z`oYZOdeFu%q8V1>RBisRV`4g^GNxe zv#3!jm;sJVYT4xuAVgE(MMikHj^7P z`-rl?oRpiq@M4e#LB{jLYpx+dGx=Ki?Hckjc}qETJy}7XQ$D($JOgjY53MB&QqI3# z5S0I3OKu?dD~s2W&b)mvKAM7c$PS205Q*r+sKj37=j+H+{wVamIsDE#a-)viGQ6mR zr1Rys0*!7*a65uK5ZsC2CLDMdVs`@={>5stfJ~t~kgyX012D1JzE26u#HMKxlrLp+ zSg#=0kaDDx)E6HFywz4f1r&M+JMzI2^kZy(55a?qp^Fq>oAg}R{}6KKV1F)x!|Fi9 zjv$tY*u#k7(T3iS*dqwk@}Uc9k0}8VtA7%xCBpC{UF3(FtB+8u)=)JSs8v)vK*cHy zRfwrUa28A-)phsU9Z^HK-P`SQZiwo=P$jdw=)=mMjpU(1bv_0oqFV1}ubNe&dZ*hf zL!~)IS>8jolY5ns9&%6E)&PHz;RZWJ8LG zPlB0&7Ae>ClJcNxu@>Y|E0?ODsL55`*KZ-`BJ9QYBC$6M>RnvDihi>Z0DwDc&g5gOi%EDP}5)v6^CV zrWgh(R0p86ix|c>ijj$8M4}i=C4ON~LhN6=J@M>yOKbSv4Y9gd&Ok*TvV;lfvpD|9!8E2n^xZcZy8--aZ*nBtAo=_@f*|!I$&<4x=}5_?<)Kr zi{DGsjeMc0I<-@sf~{J0lUj)1%hahIy17iVeXNhaX>~xI=B=LpfO3N- zH}d>2M?}+o*3G;=%mK?aix&P{v}nE0Vg>&?%u%xPKDMnxw+AP8;Pw=CbGh1%i+xJc zxx!pe9b9#<{oJi;zH4jw)3?pHgvP^a8Z@foCyoRu*J#SMKT$qI%5|hHeEcWMXVoWg z3fo*0)F+opG!zTL>1?}qqM%dPqt$j*D%IJy_!ma(ohAL!z%8m>j%)?9j=$0s<1bMy zl3vR>Ks*Fw4^bO$4(gxB1=QE(yaLZm&$MO z>Mhd-U88=Et^w3|Tz-OoK-F3!Z>qlmF`P9N^A8%PgUr(!lfyzV``9k@=#>%pV~zbS z5lBtNxOxjD-nAyCUops$U+D-RH(r$<;=gJ94$}Q8%>y8-FtX4gz2oRtG(dhQe`4gc z5%Rl{X@n!JNFCRu6s5?=Q)HCIF$2W-+n3uR+C)RH$I+OPq&kw!{Af#DayYiV_@}e6 zOvIqF*ySr%7qV#NcKW|=$<>e1Q5Qh?Y}>Q+`Tcmil}SXB5i=tMO^aN*Z8ONDk;?X8tHZ7=ZHt*hVhrRs0{cC?#dw_2^FjkpQgD&7F56t^n!RMJvim80#$z(p^=pcJ5HM^ znB{56Z|0LvWJIl^POAwz5S%dxeC3gm{N)pWiC#nWT7q>1zayV>_}>562aofI|FlDS zg4p%^fBuw~w1Gq)f<2K@A~HtC+9q$}!%h}O-w6rZOzJytsG}UrgG`jsg=j5X+vT_E!GG$@CCyBM5T*S*JG1kilPnKTUoCiD&uw z_c5jTu+u``jqT_CCr;0kl)cpVMV|9vmV6M!z5e+hegzr(Pz)NpO*Iva#Hn^Y%OFF_ z2o{Pnv`cIs_nys`e~ZjJ{@s$U>a|_P$1pZ@xX-Ic<&R>BLu$hw8T5$TWf&TmG^Cs8c$AOGeb`lvi&^AU>o>Df4HjOVAmA1wbT zip%}qf4?6>Nz2HtnDub~&4Kd!D6i%p-P{3x-r58og;KYmL9MFrvGKwQkS(7P-ACTi1<_SGkU^j_J+@F zoBpvSQgq=A#e+tOk-tS7AtuH`tY|Spmar{=Fmc!j@%(aWKgp-(JM;TfJ3(5k} ziX-&YM<(8|#8Xwnj6$CZqa9xn6EX#}ahA)ex){qPJMC)9A%|Mop>?uJLQREVz;Ll6 z6>Ld#yzJ5JbF%)He3%whxm~qRUnMINXHp?$uI5=`eO`}~iKW8ra@M%ia;!{j6!koZ zj)P{6E???uVl#;Rf=H>t&6d)zH;B>-(=Kd!qY-%f8_~jLgGePDbLC|@&}xJE<}{ki zRQ3hgU!%tD0}eMyztcbR`i-x+VvD9Pn#BG>F=vFj7uNZb)F?P{@=l+I@ZPrQCJ6*%h?7?|55BK_)yZMvj7pSTbcF{fy0gacLCn8gr)r1!aaw zrQ)l@*@)*ModPm|U?4#zLeRX(<92%*>T11guq4`xAWEDb4gKV7RE`MzZ8ZEHz$re? zgLCpwq!tB^je*&EBkfjMa{?bWCr0Fsg-62r_Q%?{KW~Yr$3n9_5<4snM2~|n^s!X9 zv76j3+{R%N_JMq2t_rp^6?K-*>yDuPPA(_QECic6CqUm1(OJC%Ta zGCU+H6R^>(1X>D#OO{-0+)62tPy`84o#eG|e#YDLn^pvmDU;FiRVDCJ5t#J%7T87i z%<`P*nE{!xCl#6#IqNI1DA|EIMA%G7QKq4_(Gc+|OgtpqZa*~s-M zflo`|raoMIyZrc4Xb@LRpc*=10shXrE)Kf!#t9656!^jA=>hxx+&D7+>w zL@GUyIS=X$Qz%A3gR{=(naX|);Bd6SC2CcciYm(`7=dixX?J6@7!ym=b&B(k!9Xi{ zvWMS}bA%Hfg-%iJgdwsE`;7~9J7HxgtP`m<@UO}VG_D&ezHEV9k?4UqI3PxP;AMyq zfAv6t#fa*@pI&<5s6|hWVqGmPf);VP7GhwzkeA`*+KGPW1?(exf44wkdOOO+_9SK^hpR{tL6g?V!$hNOqo#HNI!MwA zqC*^MgGAhb58L3^$(n19%AnrcnTX%Guil^c-dM)?@UVWAtoe^fF`g)M6_y5Yqk5=+a?Sn;8{j zMsv; ze+0fY6^IiZV6q%W9rn|CuZIRc>45D@R4G8jRoz|amI52s!', methods=['GET']) +def get_template_by_id(template_id): + """Get specific template by ID""" + template = query_db( + "SELECT * FROM message_templates WHERE id = ?", + (template_id,), one=True + ) + + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + return jsonify({ + 'success': True, + 'template': dict(template) + }) + +@app.route('/api/templates/', methods=['PUT']) +def update_template_by_id(template_id): + """Update existing template""" + data = request.json + + # Check if template exists + template = query_db("SELECT id FROM message_templates WHERE id = ?", (template_id,), one=True) + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + # Build dynamic update + fields = [] + values = [] + + if 'name' in data: + fields.append('name = ?') + values.append(data['name']) + if 'content' in data: + fields.append('template = ?') + values.append(data['content']) + if 'description' in data: + fields.append('description = ?') + values.append(data['description']) + if 'category' in data: + fields.append('category = ?') + values.append(data['category']) + if 'is_favorite' in data: + fields.append('is_favorite = ?') + values.append(data['is_favorite']) + + if fields: + fields.append('updated_at = CURRENT_TIMESTAMP') + values.append(template_id) + + execute_db( + f"UPDATE message_templates SET {', '.join(fields)} WHERE id = ?", + values + ) + + return jsonify({'success': True}) + +@app.route('/api/templates/', methods=['DELETE']) +def delete_template_by_id(template_id): + """Delete template""" + # Check if template exists + template = query_db("SELECT name FROM message_templates WHERE id = ?", (template_id,), one=True) + if not template: + return jsonify({'success': False, 'error': 'Template not found'}), 404 + + execute_db("DELETE FROM message_templates WHERE id = ?", (template_id,)) + + return jsonify({ + 'success': True, + 'message': f'Template deleted successfully' + }) + +@app.route('/api/templates//use', methods=['POST']) +def use_template_by_id(template_id): + """Mark template as used (increment usage counter)""" + execute_db( + "UPDATE message_templates SET usage_count = usage_count + 1 WHERE id = ?", + (template_id,) + ) + return jsonify({'success': True}) + @app.route('/api/csv/upload', methods=['POST']) def upload_csv(): """Upload and parse CSV file""" diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index d4a13e6..8972fbc 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -65,7 +65,20 @@ function campaignApp() { smsTest: '' }, - // Initialization + // Template management + selectedTemplate: '', + savedTemplates: [], + editingTemplate: null, + templateForm: { + name: '', + content: '', + description: '', + category: 'general', + is_favorite: 0 + }, + _lastLoadedTemplate: '', // Track the last loaded template content + + // Initialization async init() { // Start monitoring connection status await this.checkConnectionStatus(); @@ -74,6 +87,7 @@ function campaignApp() { // Load initial data await this.loadAnalytics(); await this.loadSavedLists(); + await this.loadSavedTemplates(); await this.loadRecentCampaigns(); await this.loadFollowups(); @@ -308,26 +322,217 @@ function campaignApp() { } }, + // Template Management Methods + async loadSavedTemplates() { + try { + const response = await fetch('/api/templates'); + const data = await response.json(); + this.savedTemplates = data || []; + } catch (error) { + console.error('Error loading templates:', error); + this.savedTemplates = []; + } + }, + + async loadTemplate(templateId) { + if (!templateId) { + this.selectedTemplate = ''; + return; + } + + try { + // Convert templateId to number for comparison since select values are strings + const numericTemplateId = parseInt(templateId); + + const template = this.savedTemplates.find(t => t.id === numericTemplateId); + + if (template) { + // Set the selected template ID first + this.selectedTemplate = templateId; + + // Apply template content to message template field + // Handle both 'template' and 'content' fields for consistency + const templateContent = template.template || template.content; + + if (templateContent) { + this.messageTemplate = templateContent; + + // Store the template content for comparison + this._lastLoadedTemplate = templateContent; + + console.log(`โœ… Loaded template: ${template.name}`); + + // Mark template as used (but don't await to avoid blocking UI) + fetch(`/api/templates/${templateId}/use`, { method: 'POST' }) + .catch(error => console.log('Usage tracking failed:', error)); + } else { + console.error('โŒ Template content is empty'); + alert('Template content is empty'); + } + } else { + console.error('โŒ Template not found with ID:', numericTemplateId); + alert(`Template not found with ID: ${numericTemplateId}`); + this.selectedTemplate = ''; + } + } catch (error) { + console.error('โŒ Error loading template:', error); + this.selectedTemplate = ''; + alert('Failed to load template: ' + error.message); + } + }, + + clearTemplate() { + this.selectedTemplate = ''; + this.messageTemplate = ''; + this._lastLoadedTemplate = ''; + }, + + // Check if message template was manually modified + onMessageTemplateChange() { + // Only clear selected template if user manually modified the content + if (this.selectedTemplate && this._lastLoadedTemplate && + this.messageTemplate !== this._lastLoadedTemplate) { + this.selectedTemplate = ''; + this._lastLoadedTemplate = ''; + } + }, + async saveTemplate() { const name = prompt('Template name:'); if (!name) return; + const description = prompt('Template description (optional):') || ''; + const category = prompt('Category (general, volunteer, reminder, gratitude, followup):') || 'general'; + try { - await fetch('/api/templates', { + const response = await fetch('/api/templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, content: this.messageTemplate, - variables: ['name', 'phone', 'date', 'time'] + description: description, + category: category, + is_favorite: 0 }) }); - alert('Template saved!'); + + const result = await response.json(); + if (result.success) { + alert('Template saved!'); + await this.loadSavedTemplates(); + } else { + alert('Error saving template: ' + result.error); + } } catch (error) { + console.error('Error saving template:', error); alert('Error saving template: ' + error.message); } }, + async saveNewTemplate() { + if (!this.templateForm.name || !this.templateForm.content) { + alert('Please fill in template name and content'); + return; + } + + try { + const url = this.editingTemplate + ? `/api/templates/${this.editingTemplate.id}` + : '/api/templates'; + const method = this.editingTemplate ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.templateForm) + }); + + const result = await response.json(); + if (result.success) { + alert(this.editingTemplate ? 'Template updated!' : 'Template created!'); + await this.loadSavedTemplates(); + this.resetTemplateForm(); + } else { + alert('Error: ' + result.error); + } + } catch (error) { + console.error('Error saving template:', error); + alert('Error saving template: ' + error.message); + } + }, + + loadTemplateForEditing(template) { + this.editingTemplate = template; + this.templateForm = { + name: template.name, + content: template.template || template.content, + description: template.description || '', + category: template.category || 'general', + is_favorite: template.is_favorite || 0 + }; + }, + + cancelEditTemplate() { + this.resetTemplateForm(); + }, + + resetTemplateForm() { + this.editingTemplate = null; + this.templateForm = { + name: '', + content: '', + description: '', + category: 'general', + is_favorite: 0 + }; + }, + + async deleteTemplate(templateId, templateName) { + if (!confirm(`Are you sure you want to delete the template "${templateName}"?`)) { + return; + } + + try { + const response = await fetch(`/api/templates/${templateId}`, { + method: 'DELETE' + }); + const result = await response.json(); + + if (result.success) { + alert('Template deleted!'); + await this.loadSavedTemplates(); + } else { + alert('Error deleting template: ' + result.error); + } + } catch (error) { + console.error('Error deleting template:', error); + alert('Error deleting template: ' + error.message); + } + }, + + async toggleTemplateFavorite(template) { + try { + const response = await fetch(`/api/templates/${template.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + is_favorite: template.is_favorite ? 0 : 1 + }) + }); + + const result = await response.json(); + if (result.success) { + await this.loadSavedTemplates(); + } else { + alert('Error updating template: ' + result.error); + } + } catch (error) { + console.error('Error updating template:', error); + alert('Error updating template: ' + error.message); + } + }, + async testSMS() { if (!this.messageTemplate) { alert('Please enter a message template first'); @@ -442,8 +647,8 @@ function campaignApp() { return new Date(dateStr).toLocaleTimeString(); }, - // Load template helpers - loadTemplate(type) { + // Legacy template helper for backward compatibility (if needed) + loadLegacyTemplate(type) { const templates = { initial: "Hi {name}! Hope all is well. I am wondering if you got my last email with the next couple weeks canvassing shifts? We are out every day and would love to have you join us. It is last minute however we are also out today, 5 - 8 PM starting at the Old Strathcona Community League. If you can make it, please let me know. Cheers!", followup: "Hi {name}, just following up on my previous message about volunteering. We have several shifts available this week and would love to have you join us. When works best for you?", diff --git a/src/templates/dashboard.html b/src/templates/dashboard.html index 4721c0b..34492ff 100644 --- a/src/templates/dashboard.html +++ b/src/templates/dashboard.html @@ -10,7 +10,7 @@ -
+
@@ -57,6 +57,11 @@ class="px-4 py-2 rounded-lg font-medium transition-colors"> ๐Ÿ’ฌ Conversations +
+
+ + +
+ โœ“ Template loaded successfully + +
+
+
+
+ Preview: + +
@@ -322,13 +352,119 @@
+ + +
+
+

๐Ÿ“ Message Templates

+ + +
+

+ Create New Template + Edit Template +

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

Saved Templates

+
+ No templates saved yet. Create one above to get started! +
+
+ +
+
+
+
- - - + + +