From f4cd9603fa8d24fff73bf08b3165c168dcaee681 Mon Sep 17 00:00:00 2001 From: ErolHaagenrud Date: Fri, 19 Dec 2025 06:58:34 +0100 Subject: [PATCH] =?UTF-8?q?F=C3=B8r=20effektivisering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarDetailsBottomSheet.java | 39 +- .../main/res/drawable-hdpi/ic_stat_kbs.png | Bin 630 -> 762 bytes .../main/res/drawable-mdpi/ic_stat_kbs.png | Bin 395 -> 504 bytes .../main/res/drawable-xhdpi/ic_stat_kbs.png | Bin 895 -> 1037 bytes .../main/res/drawable-xxhdpi/ic_stat_kbs.png | Bin 1475 -> 1616 bytes .../main/res/drawable-xxxhdpi/ic_stat_kbs.png | Bin 2326 -> 2176 bytes .../layout/bottom_sheet_calendar_details.xml | 26 +- hele_prosjektet.txt | 67 +- hele_prosjektet.txt.old | 5756 ----------------- 9 files changed, 117 insertions(+), 5771 deletions(-) delete mode 100644 hele_prosjektet.txt.old diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java b/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java index b5b01c5..768775d 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java @@ -1,11 +1,14 @@ package com.kbs.kbsintranett; import android.app.AlertDialog; +import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; import android.text.Html; import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -64,18 +67,50 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { calName.setBackgroundTintList(ColorStateList.valueOf(color)); } catch (Exception e) {} + // --- BESKRIVELSE OG LENKER --- if (!event.getDescription().isEmpty()) { String cleanDesc = event.getDescription().replaceAll("#varsel:[\\d,]+", "").trim(); + + // Konverter HTML til Spannable desc.setText(Html.fromHtml(cleanDesc, Html.FROM_HTML_MODE_COMPACT)); - desc.setVisibility(View.VISIBLE); + + // Sørg for at rene tekst-lenker (uten ) også blir klikkbare + Linkify.addLinks(desc, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS); + + // Gjør at man kan klikke på lenkene desc.setMovementMethod(LinkMovementMethod.getInstance()); + + desc.setVisibility(View.VISIBLE); } else { desc.setVisibility(View.GONE); } + // --- ADRESSE OG KART --- if (!event.getLocation().isEmpty()) { - loc.setText("Sted: " + event.getLocation()); + loc.setText(event.getLocation()); // Viser kun teksten, ikonet ligger i XML loc.setVisibility(View.VISIBLE); + + // Gjør adressen klikkbar for å åpne kart + loc.setOnClickListener(v -> { + String location = event.getLocation(); + // Opprett en geo-URI. "q=" søker etter stedet. + Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + Uri.encode(location)); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + // Sett pakke til Google Maps for å foretrekke det, men la systemet velge hvis ikke installert + mapIntent.setPackage("com.google.android.apps.maps"); + + try { + startActivity(mapIntent); + } catch (Exception e) { + // Fallback: Hvis Google Maps ikke finnes, prøv uten pakkenavn (hvilken som helst kart-app) + mapIntent.setPackage(null); + try { + startActivity(mapIntent); + } catch (Exception ex) { + Toast.makeText(getContext(), "Fant ingen kart-app", Toast.LENGTH_SHORT).show(); + } + } + }); } else { loc.setVisibility(View.GONE); } diff --git a/app/src/main/res/drawable-hdpi/ic_stat_kbs.png b/app/src/main/res/drawable-hdpi/ic_stat_kbs.png index 0aa6148b1cdf676b2440e7f34bc273bfd66a168f..e92dad05570ed53f813b7659c76589cae1f8ee53 100644 GIT binary patch delta 738 zcmV<80v-MK1o{P#B!9t4L_t(|UhSAWXca*ig%cwrA&R0P+S^#Dos|-TV4)%yOcW$w zW22yUwn0c|p|&DgXr~sIA~vFhAmaP25%G!d_xpH$X0OgY&hE_Yrm*mXk0iVQocm{Y z?%bW*(ea-|04$|<=r{U?KBf2R4SJd0q~r7veL+9d5A+c2mVc~O^d4N+EBX*F`zpOo zAJEsu`R8<$cKR6>VlQ5oAup$kKGuQ+7W@5J&|9s@V8qMmqhGWjfkD3?3mVgkM9$Kw zlC@WhB3z-<{R|7~qyuypz13D8<_uj#yV`>OWG#W4Z&jS4>uIlMw84TNxJg{ab{*%# z^BZ-5hfVYm=zm16g2e|VMA@QaTJYQol+UBvbe@ih!a@5qNeDIs*UTp^E;vtT>zE`^ zqahw!j`C(s(-}G@8DuuZzgNR8mLZPQDSmx6$Y_Ya52Ad|9HBjab}f+D5PzL}k8g#f-un>S0$X`==`$@X;k}Nx0NXv-_cwN5?OEBr z!?Dk*@#46BP};uVzTbIS)V-sJ=`_tVczhu}2{#o?%ezMJ!hJVn8J6|eKlD%V3o;k` UOTwfnvH$=807*qoM6N<$f+$dO)&Kwi delta 605 zcmV-j0;2u;1@;7xB!4|gL_t(|UhSAybA&JqgfDf{+obp2pYs3zo3c8YjM>rqtgtCVvw=Ab*qR#a7(XoY5!I%{37y>hINW~dzOD5i2zoS#Kg>sDE3j_0`ZT@)@zU>4WtqqK4A; zTb}b!(=uj~HyYBCwOVwgcF(MbXfc%bFQia@H))?_VeRaS8dcnK^89ca6WxMh8MW~@cwzUjcun diff --git a/app/src/main/res/drawable-mdpi/ic_stat_kbs.png b/app/src/main/res/drawable-mdpi/ic_stat_kbs.png index 789bb4b61c91606696523936dd2dac121b904126..c904ecc01efb83a3583222ff4293cc83c902d025 100644 GIT binary patch delta 478 zcmV<40U`d21NZ}wB!9n2L_t(|UcHt}D}-Sb$44$TDGQfKSrAJsWhV>87no$D*eVvv zf`z55l~|#C0cA&Q>?L-}T_Lwza_jh?o-?mA=Y5`c%z|J2n&~~4ztOzUJE_!vKtyx! z9X`Vwcm~hmHGG60a1T~Ve;quKnLBtXGyI;f@oa|Gj<#he958I*u8A`)3)0icb)9m_NDgH+t!XDcjfnu#0$kJ!}8sV|&*@l(6 zF9x}f?js5yxBSS|*I-)j@$_K?t8A8 zfkk#lA`}kb8-Go+ONB0HC>-Ep#Lnl13Jc)7zuoRggbE8__&h4NdxB6nU>fe&i^8(q z6NJJ6{55TY54IqT+g)BLAwcQ_wjl7KH)Qw4pgaN6vto-v{tz@Y*eO8=RFFG0eI$pV z*lQ<*2~sx}-glCFa{1tpVZE0JO5g&;&vy+U&x2}a(pNSpIHX?(blZ*qPb7yMzrrn3 z*Iemh1ikb9-!Puo^q(|oQ@Fry zCIdZ0YZ4hWit~6+wH6W>{E(jOr19iUwALjsDDIciq}*4W zX&Kbu>#ey6+5!niEmY-_jg6^!}tXn4E~8u>-QM| O0000yKCHdgl$G_+gCERnFth`D5B-4NK`)?(&~4}nbPl=zU4MmcL9NhZ=oR!C`U$l` ztJI(1crR!T^dukq0eS{KfNnyU&DCJ7d(iWI3}ZW>ozMvN&4Gbk=9i{`8cQ`O0sH_f z)SKpCH8LgZ0&48lpalGZYSo+O{8Rd*C7_0nzcNqn8ug}mi@EN6a2*;HP-8muO=9D+ zO@04?!8y(W;0$_vvG(R!sp#dBp z2p!en0C)$h)cjOf$_8+32y{jR1>R}aL%lRVHI||Q{9FTF)F9zEwA#|#X~M9C4dDA> z&@~MZ_;6ZQSkF6_paFaxX>O_=2B#DAw4Qe?W&=v_kr zYk+W>WQ^AIie=jX;Sx#F*U!{g{02^%TQ!KoMy=}&i`T$;b8lR0b)W>C&^2pqFIc<= zI_E-tpc6V+0xqge*7~Wj1P$c#MnP{hpad_?$A5cpA(pU#d~TDu(<2UT(C9*+R9K1z z^7$JyC<2_#`f0+_H^7yMQg3BJa8_HVezCEX4G+xXiH*a4^~_;z z>&GYDI}TiJ#__3^e7~$i{c>PnxkjestZ;>2)XnCuo4B;9R}KvHHg^GXhlHOF&AH*n zI{+Ul_n{l+rU%?n=gvBJ*!k$^Y?86R%w25U+v>ibD%984;`x3Lae@+R*Kc}+J9n0btast)rk3d=iZ zaRP6EWljH!VB{D+;NEF^3iwQsW!&;@T6|Pp35HoO-{qO64`PKbSc&Q}klDZCGt{N( zisf0gxUqPyX@A*R;SUx|s>jGNz*e;QHq3sb09(`I zysIhj4di#!Op!U%8nD0D)R1E7i{a&)R_0!64A{RL7JsLj-o?_FkTIy6DK%iRW${VV zx>))O2FAr`6dL$xajbe5_!i>PIbkvi4GbQJEYA!(kL?$%AjopRnqcfEO0zY={s6HxF8X)k~)^y{c)Bt=(`BL>hka0$+ro;gJ zAi@n?(SHDipVF>XAB92#4|^dF(B56B0^j`guHseH7zlsAMZt{*VSYsJlA5SB5dP1* zpZG;=0?WT&YP^?51L=#{9<0p4XeL?=1daRt;@v0wI;VY&Xf;4g-?i9QeF!VF$Fiyy z5h}@AXi^3Ue0|8ecI*<&@2j4T6+X&+@PGkc&VSyjo&)@rZtxy83SSDDi&CEfV(GkM zsk$zNPs<0Y_X5B2$X+4ghk16Dd(qdip9N?S3Bh?=fT8g*QZ2h7S9?Z{&z^}uj ztqs3%9z1@=fS>3NRsRkA8n9>^kODr1UPW~NvQ^`Oz{f;E67~R3U yl71;K3D&`hU{jx=;Lo7fI#R)Uqrd7C|ASvHm2!qx651L70000Y+#^ zUZ{{X5h5XJ!~+kA7d3}OXfzS?R8vzSHP%cJLs3HwL1@erpYOA73_TJ}2 z?l1ZBPwv@!t@U5`oPG9qM~xaXWXO;f0bm++9rX#dk@}hXntxhHy+gf5Jx@JJJx1M6 z-9g<=J=po|3F;Z@Me2=Cy^pDHsb8tzVD@gHX8IWV!E&>yWz?tC@6=D!7u0*y>(mR> zYHB6*0CgvI5A|s0bH%;3Q|AZjPgp$9QKwU5z0V$4Xa@Bm^0J-$pQxOy@9 zn_5nd^S--afqz?F#avwFV=2PxD}03nC`l)I-(A3NxhBx#G1|u?`1#h<_pW>@mV5tQ zu)r_AMkZdx<`bmHUhP&6R(StizyfW8n|v&r;I%2pcV{p4{<~m-MX+ZvCwimo;bSSn z>sR_p3D8}7fcM=63yh&IrT*xyn*_aP)%MKyad=o}GJkb5(#7=^wZQxB3k!~>=2F*F z?;?Is>KC0u?TJCJ)dKi0u1l!*3-+p_zSL9H8PvAkUny8|9JM>_(bGRx7d^gnyq^$Q ze?0ZHvm1iGaCK+Yx9a<1s058oh22qgP-kxoZ@Vn8yXazPGX%wdxVIY(8o$7M3}hWO z-rFq;>wlgCYrCqTVa*h8Gb(hjzP7&5S3f1~)dZf-m%*SbS(8;0QzJsZh zAl5ks)*T4tJL+g}FBx=pu#X^+-~_S!vDA;wP8mL@=6QQbLFNRp>~XMZjZi+I_V)IY zgNzA+wRct8oHwamy}c|TQ-WCLL{uA~Y9eoYZ+|Zv$dDjdr&PV%P&AXcmA97_Bu)^o zEk?DW&F$2d-d=W)C_%6u^4SPaj)pGk08a2W8vzLt1WV4j2=5{PMO}tI32zKUP7o|P z=OKI!O36H*8dHp~aS$;3^j*Bm|ACj`T6L3e-=Kh5%O}JULnoJU`2?wvKh4M08qj=#gyyDet*s>k z?MRT&CX2~FzQ#d&5+t-V-mdlt?Mjf^qkofqjE#bJB}i!B(1=@SvVu4XQqNeM52_41 z`k`H_lYFd=fH(=dg_=m+=02~(wP-_og!&26e(QfX5b*DIu+e=CbT^*qjvgs7nvjawH__CD&bsUH-aM3Omw#Z#5n>KS zis&eO*en1m>;sz`P4WB}k$oNfA93v+wj=HG?y6DIj~|p9;IiOM*q)r6Y@qh@u@r$# z)oQbNDs%#IqW9Ya3ti)DW@4p}#lteQV989y67RDI7JAFq%7jiN4%{sdJ6IQMRm8K# z`|APr10qeJeTW0MxWL*Yt$(bhV1xJ91MC-5nn3MKsnHbn6LFR zXxGMJK9-Sy9W=fV@!quMmyck-*K;*&aHm^eyW2G`QsVT6`v^9s(M}7^+^GZj2rOV7 zF1eR_n0g%6UC^A2P91Bwp+;7{YMlqu`0Pe%FCRl0V3Ym}VSd##s5vvc>9>XCPZMy8 y>n_-n@ht32naU~-{a^ZW)}XB$)Q1fD|MCwi>cRO^MR1M)0000oNkl5>&i5QT?LL3R)WWV^B`I~a(ds0f0rZajqV z{|3i%CTrwQ4!66iy0W`}+%F;nB5S>;yQ^!-o{fz)Yu2op5dikkexkjg{T67?lO6Sa z_!!K00x^A3o!|KOJ_k$A9ecX+vS1yCe|pSD&_n z)QN-MA4%%q8}HwOX}i19TfsK2(yn{|HkjeL_l<$B9&NAp3;MsH-Sz$m*v<>@+lKBS z?;8WQ`+#=D`vv{CC6f0?(EIy)6%0ZhqwV&7LH`xn!uumY-xCBIFhBABNZ8&1wd?&` zs6Soltza7`X@8Hre;ds3DeWKq>(e4&J7;Kr_O-|KTeOGX-wN0`_Xk$o#oJ(}{jj;g z6WV#&E}v%ztnW1K8SHMom({6_&2ndHuW7%~&iM3F)GqA??IrC|Za@A9Ce$_9WKM#0 zzXCIW&3oQqzfkJH|Je=e{`Ruf@epmsL^Y%=x0`+rMd`BND1%GQsey`dfOX*sam zA5 z?V9)Zfe$a9<36o9Y~&8@bMMckR-hzK@zsYt(tlyaEWJ6f*KPHzwniZ4|BiOk`}<&- zbFhK9dVfeLW^93AOOPAh-v?~={lxpj8B-wUd#L>0-v`UwMBj`!im?P@o*$9BiM0f5 zth(p@qcMg+uo?U%@9%?U9;4zd*OX|1U`vqm-rq+mS3SRKMwCFX8L}pFMU%i(PV2M5 zYqKUq2$X)LX6QSF>jEFvbtinF5K&+0>HDNqvURcU?9 ze-Ji1u3E5$(n=uMWAzq=%NFK82D@|i`LtQkNFZ%|=|>hV9@TCVwQOT+Od1HJ9lU?B z;sj$A%zptB9VishK%he?99Uud*4?KKb781JHNZ|i!cSJ4Foi&ZZ7yi{y?=I0C6It^ zw|`F3b~aNABrxXgU=1@6NRz}@-ai|r7D&K$3#*TgLYad=0=8##%KIm0MgnOar?d2V zjYo44NU$GAFt^$DoCFfE*nwN#KN&3qg018gD7xRL)qsLX*>Hvml*G{{VG%s?XmZdN`OFm zJl>&ETx9Cs9q%8F)&eD;mJctNeM5i+I~1SFHX%Zww1ccQ`$iU3#8?P=10xmL`kFz-XBJ!K!RyU(RlwEwy<4ze+bb6iPXl|-aiCbY=3op zaKjh^30QFVA@3goY~a4^{Y`;I#-H^5TnYrzhke@4Xf_@RSPaos?{5YyX0Yg7Ynfbu z1niTA<}+i~`2(WnN$Co7et6PFi9jMEaKD`!L_?)&&^~g%B-nwCPnUIqV)_b>Dq@=7mm35VCfsHfj zpmrKNV}D>Lt0Db?4cvL*$FL9iLvHC+z)mST$2g`RM&S3WuqWl1_wPWRz<+?ZRV}c^ zW#{G@Y^qTF{YDP3v9|LfZGyh<$Af@h($ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_kbs.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_kbs.png index 819488a876c76bb5e76506aee2f9009a42713d21..126084bc69503df8003311d3c38cfa8b3da756fa 100644 GIT binary patch delta 2163 zcmV-(2#oiZ5`YnqB!5RqL_t(|UhSQCs2xQV#uL*dQ4>vqiBY411u=gd0nrGe zf}jKu5k;^B1;zT05fu~-_J%~oE-Ln}vDetJEAs5I$KL(?KF@pba&m97dv|7M_Hlpk zk-YbIcFx_icXsy7%$Y%hx^(H%rAwDCc>!P&bq4h+^%M0MwSS!YlzNwXje4GXoO*z| zgSwHrn!1cSk2;%LL|sH((fhX>sN1N!sRyYg)HBpe)Z5f2)c4e%)X&rhz2)Y38;ihl zL#RD_>(se^p?;&jq~52NQcqEjQ1?)`QrA&e!t6emI)}QLx{A6P_IaO%T^pTq8O+wd zVds5?I*Z!M+kaCC7Mu;c&^aN8$eC++d)vWdN5U?oE=&q^!yZBn_O`W!2X6-RXpa0# z9q4Ut2ajFYTP7v?w(a9>YYXhFmNPF?!@S)d9y0;f9OS_3)NpTOOL$<9FP#}`iq`RV zdtmK;>bt*>Kd5Qm#+LLRoI4wPyFEN+fiIMpdEUmBAb;QXzYpCYMZN(!IY6C&S>DE$ zz`9~N^8_`-+wI{oyHo$TvZ?ryn&fS42@l*0`55kzU3++&TfyVjqTX=jQ*oQOuPr=y z4eAo)=LB`w3G&0t-h>p^k-xJz{lFW%qN` zQPhU$;D%`nPd1L4OPvaf1;zRhLUE&T#`gmj+X>CDu=vq*ln9k{3@pa7xW5H~+1J+@ z0-kp_gpCX7Ce$52-PaWzRyvycXhnO1G27Qy6@Q*{Q|dEcQ3VwJ)N^u#ud6m-51AND z^>tN&=Nk_T5r^U}6b&uJghm|cEeph!zOGK;xz?fX^A%J;9g+#Yo+Qwa`QExf1aSbY znbbf`2-E|-lp5*lNd}GC+Zh>($_{{C{M-GBJ4ErP6RDxTo}|FS7BQ&g09dngr$1?j zsDF!jI3jh6D}f9h0E+>I3MgtUe7>(E8<3d;VDBdyFb{#oa|;Fjl7WmI0Bd&CLkWS# zM2f}lQ-Vw!01K07T1g0ckV7B82*|(zuz;cF^@Kq4k~a5sWD62IfX8i2z2FOX$crd2 z)|?ec=l~uw88)je1dmbc`#Q1*i5vjC_+K#eAb$r4&10SDZ66Sz84eJ)Ah#(cAwY8+p!m-MZ-3)Ka~vR) zqM-g+%rQZ;93b>O80|ktV?pyAAb*tjqZz`Me1keZ&&jE^Lo<@zKo^*eU zf##O3?LS}kAf5x1=k5PFJN$PW>TCD+(8%Gcvsv#4i06P6LI(W#AcS3D-G2(b9~{%a z-t0j<2l#nGLt!Z+WAi(;YyY~l1~oaLx32Z5cl?17&_Mo5^=Aocbb#RRcYt*rW22zm zM!x>+K+O&iu%t~fGqv=g^q6_C89*8b=y`CjFA#|(2#~KR8c5&(0b3)WOj>oIY+Gx2 z8?pe293WtItJe}6HFp|Q&VSDdNb3M47gSoaI-UorLrFjKasm=MK)@DfKkExeMC-}& zS<-~W4iK*RiM3}V&Wl)Beaev;HT42S;dusyG zkpl!QwOF0c1EEYU^*e!;p6M+KL=^`p?{%o8O=Zw-QuQ`Rh?hNghOes(uv`)$le88^ zd8zA(9l_p2wK+`;DyJ@nWpqrj9g9*K$8U~U$##hMz^go;pXCQ5PNME2Xf{W@eYL{wR@Dv29C=0K20mQ%ArOCDqZ>P_$EN#FW`Ysqp3#j;YYMqK7&KCEHk7j)=N8pYUkU$9`w3oPn62Nn*{#*}Jzm4Q%B(GC-8rZh`U6Izu{Sb-?T z#ubxSJm)6r8tQWDeClj!5$xNgMG?wgtKi#R)Wf~y-)w~Ci&HO5;Rr=FwUkJ2m8Vl% zc{{7X^NoP*z%4tq1FOCj>VRmrMGq|Mt5r0b5uveX*{=nD`v2|Jqp)kE=Zcy&1u1oX pWV4#dqtQVarAwDCUAhcx{sU&bU;rY-0LTCU002ovPDHLkV1oRw4G#bS delta 2315 zcmV+m3H0`W5tb5=B!AsWL_t(|UhSO;mlZV-g;`Vt5m5oz8CC%i5d}nnkxh_&Q)H2G z_vjz+`0KBAxp`+EMbCTPFG+VA`#Yx&LnWQgOZAdeD(Pj*1_lNO1_lPo1Hf^UhbE6r z9*xNlLI1tUcP0-^?wi~*xodLA*L+=SVF$it@{7r*Ca+hdFE?3XvXUoZ z9+NjrR++qMvf5;g$=XmB|E7*<6y{@o9!FsrENjIQ=i;$fpvHMA&r*ak*~k~ed6&C% zmObq^u>PapFn_B1sLpJI7s?FfGHtKPZzg+u`b3}#@xXU{TGv>e4wzi``YPBk(3QR2 zr?o{e1n-pkcduo>Y%=Hdr2yUj>%87$dA68b^ZK@d?y3c^UlPmwgjbK(mx9%8pUEk& z_dw6=wO-!^(BpHH*H>a0A9-C#Sp7DdT=II4<$3INU4LP9nKL=<^_BF%0GmxNc)h1E z1MD<8gWEBSqBlB2#wDd>p`DZ1?wMv$7-L}CU*WWOwM`zMC^cV(3SRq zPivcSL4O=GS*Y9iBP{7d%+USDGk0vKg*7!WhfPe0cdC2LM{5=7=5T1H{3PK}&?Dew} z>Pm+t@2=fGJuA>%@Qv5U0y(-KczrBvAd@>@KPy(&+t3wXZ-A8pUa3g`nBeVv*Xv`j z^4^0w74^D;Q-B_iKhX$2tyQc7*OAz33b}r7dHt+FF66&a;Z~mo$hCbH8Q+1mu}XYx zvVYm@V@PQL$cO(I5;*0co*8?D$9-C7KwZThULQj$13=Gc4#c9n&(K}O@kG+!8V%&* zNLr~B24JIy28(_?219a!syz2hi3VV!L({1#8lNw3Hh%P7l!ej^5E`FXesqIDZf_bg z-=Hd%k_^B`ho1S-T|+Bl7vy_NI$};vDSrmA{Oe8rXehOCre0L#0{ zkJliYNKMKwMf9Gdf(8s<_YT^Ilg@t_khw(&W6!Z^>PKCL&jZ2-sx&H&ih#($^F zn?0QaKCM@@X#gnLHM)BU%`q>aNxWvMv}FKZsZf-2)Z7C!*TBJAy)ePjX~O`}z=%=K zv5ikR1;-1~KB8jCZ2DmD zUf*l_WB>xKSef(s-qJ$@aJ3Ahq+bo6P~Qwdpj9%(tRaq~j|L#ns+O}}-+w#$Y5)Sc zx#JhnbxF?+z+Fr@J}5iWA7=S=ga~HinHIVz7Gih6VS`l<*00KqMB|Uc)(4de#uz&IfY8@MZj|gtW zk-gszy%QMRTb2>JGyt)D@1muBPI(T1lbc?j zi(mwf6S@m~gg*9=)PGAks{x1|cL|wY72{xWu(sc)wE`6Qn$#^2X@Jp7GHIPA5za{? zw7=#U+C?|%Bu0z@2;|qV|H+U9U~v-ZL!XuvsP~c7Cj-p|-}m|^K&vY+dwp)8<+SZ= zna_qthL0qKt0}r%D}yU7=e#~Aq3&GSdBmqB2l|r5CtjZdUVpua^v6R!9J&u$jr$2k zpey^RPfG?gbyxqZ5DkIuuCtKanP0+K@AK5bj@y6^;y^?FZ<-wQX)^Ey3mtoY5+cC=e@oppq*|T z5B;I8ZL;sl)CEnx(+Ty#qg?_yLBw7_cH9ZHyl$9xp4UwT`kq(29=@7DkI34+pemrx zT2Y$yPG$g_FuLmX6M#U+_2=Ky>diLe lT{h9U0|NsC1OGq%1N7SaMUGF*Z2$lO07*qoL - + + + android:visibility="gone" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:background="?attr/selectableItemBackground" + android:paddingVertical="8dp" + app:drawableStartCompat="@android:drawable/ic_dialog_map"/> + + android:layout_marginBottom="24dp" + android:autoLink="web|email|phone" + android:linksClickable="true"/> ) også blir klikkbare + Linkify.addLinks(desc, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS); + + // Gjør at man kan klikke på lenkene desc.setMovementMethod(LinkMovementMethod.getInstance()); + + desc.setVisibility(View.VISIBLE); } else { desc.setVisibility(View.GONE); } + // --- ADRESSE OG KART --- if (!event.getLocation().isEmpty()) { - loc.setText("Sted: " + event.getLocation()); + loc.setText(event.getLocation()); // Viser kun teksten, ikonet ligger i XML loc.setVisibility(View.VISIBLE); + + // Gjør adressen klikkbar for å åpne kart + loc.setOnClickListener(v -> { + String location = event.getLocation(); + // Opprett en geo-URI. "q=" søker etter stedet. + Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + Uri.encode(location)); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + // Sett pakke til Google Maps for å foretrekke det, men la systemet velge hvis ikke installert + mapIntent.setPackage("com.google.android.apps.maps"); + + try { + startActivity(mapIntent); + } catch (Exception e) { + // Fallback: Hvis Google Maps ikke finnes, prøv uten pakkenavn (hvilken som helst kart-app) + mapIntent.setPackage(null); + try { + startActivity(mapIntent); + } catch (Exception ex) { + Toast.makeText(getContext(), "Fant ingen kart-app", Toast.LENGTH_SHORT).show(); + } + } + }); } else { loc.setVisibility(View.GONE); } @@ -5552,7 +5587,7 @@ import java.util.concurrent.TimeUnit; public class MainActivity extends AppCompatActivity { - public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com"; // Bytt med din egen hvis denne er feil + public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; // Bytt med din egen hvis denne er feil private static final String TAG = "MainActivity"; private NavController navController; @@ -7140,7 +7175,7 @@ FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml android:padding="24dp" android:background="@android:color/white"> - + + + android:visibility="gone" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:background="?attr/selectableItemBackground" + android:paddingVertical="8dp" + app:drawableStartCompat="@android:drawable/ic_dialog_map"/> + + android:layout_marginBottom="24dp" + android:autoLink="web|email|phone" + android:linksClickable="true"/> Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.kbs.kbsintranett", appContext.getPackageName()); - } -} - -============================================================ -FILSTI: app\src\main\AndroidManifest.xml -============================================================ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java -============================================================ -// FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java -package com.kbs.kbsintranett; - -import android.util.Log; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class AuthRepository { - - private static final String TAG = "AuthRepository"; - - // Interface for å gi beskjed tilbake til Activity/Fragment - public interface AuthCallback { - void onSuccess(String role); - void onError(String message); - } - - /** - * Utfører selve API-kallet mot WordPress. - * Denne brukes nå av både MainActivity (Silent Sign-In) og LoginFragment (Manuell). - */ - public static void loginToWordPress(String googleIdToken, String displayName, String email, String photoUrl, AuthCallback callback) { - - // 1. Lagre Google-info midlertidig - UserManager.getInstance().setUserData(displayName, email, googleIdToken, photoUrl); - - // 2. Gjør klar request - LoginRequest request = new LoginRequest(googleIdToken); - - // 3. Send til WordPress - RetrofitClient.getApiService().googleLogin(request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null && response.body().success) { - // SUKSESS! - String cookie = response.body().fullCookie; - String role = response.body().role; - - // NYTT: Hent utvidet info fra responsen - int userId = response.body().userId; - String fName = response.body().firstName; - String lName = response.body().lastName; - String stilling = response.body().stilling; - String mobil = response.body().mobiltelefon; - - Log.d(TAG, "WordPress Login suksess! Rolle: " + role + ", UserID: " + userId); - - // Lagre cookie, rolle og ID - UserManager.getInstance().setCookie(cookie); - UserManager.getInstance().setUserRole(role); - UserManager.getInstance().setUserId(userId); - - // Lagre utvidet info i UserManager - UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil); - - callback.onSuccess(role); - - } else { - Log.e(TAG, "WordPress Login nektet. Kode: " + response.code()); - callback.onError("Kunne ikke logge inn på Intranettet (Kode: " + response.code() + ")"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e(TAG, "Nettverksfeil mot WP", t); - callback.onError("Nettverksfeil: " + t.getMessage()); - } - }); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarAdapter.java -============================================================ -package com.kbs.kbsintranett; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import java.util.List; - -public class CalendarAdapter extends RecyclerView.Adapter { - - private List events; - private final OnItemClickListener listener; - - public interface OnItemClickListener { - void onItemClick(CalendarEvent event); - } - - // Oppdatert konstruktør som tar imot en listener - public CalendarAdapter(List events, OnItemClickListener listener) { - this.events = events; - this.listener = listener; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - CalendarEvent event = events.get(position); - holder.day.setText(event.getDay()); - holder.month.setText(event.getMonth()); - holder.time.setText(event.getTime()); - holder.title.setText(event.getTitle()); - - holder.itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onItemClick(event); - } - }); - } - - @Override - public int getItemCount() { - return events.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - TextView day, month, title, time; - - public ViewHolder(View view) { - super(view); - day = view.findViewById(R.id.cal_day); - month = view.findViewById(R.id.cal_month); - title = view.findViewById(R.id.cal_title); - time = view.findViewById(R.id.cal_time); - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarDetailsBottomSheet.java -============================================================ -package com.kbs.kbsintranett; - -import android.content.Intent; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { - - private CalendarEvent event; - - public CalendarDetailsBottomSheet(CalendarEvent event) { - this.event = event; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.bottom_sheet_calendar_details, container, false); - - TextView title = view.findViewById(R.id.sheet_title); - TextView time = view.findViewById(R.id.sheet_time); - TextView desc = view.findViewById(R.id.sheet_desc); - TextView loc = view.findViewById(R.id.sheet_location); - Button btnAdd = view.findViewById(R.id.btn_add_to_calendar); - - // Skjul knapp siden appen nå varsler automatisk (iht krav) - btnAdd.setVisibility(View.GONE); - - title.setText(event.getTitle()); - time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")"); - - if (!event.getDescription().isEmpty()) { - // HER ER FIKSEN FOR HTML: - desc.setText(android.text.Html.fromHtml(event.getDescription(), android.text.Html.FROM_HTML_MODE_COMPACT)); - desc.setVisibility(View.VISIBLE); - // Gjør linker klikkbare - desc.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); - } else { - desc.setVisibility(View.GONE); - } - - if (!event.getLocation().isEmpty()) { - loc.setText("Sted: " + event.getLocation()); - loc.setVisibility(View.VISIBLE); - } else { - loc.setVisibility(View.GONE); - } - - return view; - } - - private void addToSystemCalendar() { - try { - SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - Date startDate = apiFormat.parse(event.getRawDate()); - long startMillis = startDate.getTime(); - long endMillis = startMillis + (60 * 60 * 1000); // Default 1 time hvis slutt mangler - - if (event.getRawEndDate() != null && !event.getRawEndDate().isEmpty()) { - Date endDate = apiFormat.parse(event.getRawEndDate()); - endMillis = endDate.getTime(); - } - - Intent intent = new Intent(Intent.ACTION_INSERT) - .setData(CalendarContract.Events.CONTENT_URI) - .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) - .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) - .putExtra(CalendarContract.Events.TITLE, event.getTitle()) - .putExtra(CalendarContract.Events.DESCRIPTION, event.getDescription()) - .putExtra(CalendarContract.Events.EVENT_LOCATION, event.getLocation()) - .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY); - - startActivity(intent); - - } catch (Exception e) { - e.printStackTrace(); - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; - -public class CalendarEvent { - @SerializedName("title") - private String title; - - @SerializedName("start_date") // Juster denne nøkkelen til hva APIet faktisk returnerer (f.eks "start") - private String rawDate; - - @SerializedName("end_date") // Juster nøkkel (f.eks "end") - private String rawEndDate; - - @SerializedName("description") - private String description; - - @SerializedName("location") - private String location; - - // --- UI-hjelpefelter (settes manuelt i appen etter parsing) --- - private String day; // F.eks "12" - private String month; // F.eks "DES" - private String time; // F.eks "10:00 - 11:30" - - // Konstruktør for Retrofit (Gson) - public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) { - this.title = title; - this.rawDate = rawDate; - this.rawEndDate = rawEndDate; - this.description = description; - this.location = location; - } - - // Konstruktør for manuell opprettelse (f.eks ved feil) - public CalendarEvent(String title, String time, String day, String month) { - this.title = title; - this.time = time; - this.day = day; - this.month = month; - } - - public String getTitle() { return title; } - public String getRawDate() { return rawDate; } - public String getRawEndDate() { return rawEndDate; } - public String getDescription() { return description != null ? description : ""; } - public String getLocation() { return location != null ? location : ""; } - - // Getters og Setters for UI-felter - public String getDay() { return day; } - public void setDay(String day) { this.day = day; } - - public String getMonth() { return month; } - public void setMonth(String month) { this.month = month; } - - public String getTime() { return time; } - public void setTime(String time) { this.time = time; } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarFullFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class CalendarFullFragment extends Fragment { - - private RecyclerView recyclerView; - private ProgressBar progressBar; - private TextView emptyView; - private LinearLayoutManager layoutManager; // Trenger denne for å scrolle - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_calendar_full, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - recyclerView = view.findViewById(R.id.recycler_full_calendar); - progressBar = view.findViewById(R.id.loading_full_calendar); - emptyView = view.findViewById(R.id.empty_view_calendar); - ImageView backBtn = view.findViewById(R.id.btn_back_calendar); - - layoutManager = new LinearLayoutManager(getContext()); - recyclerView.setLayoutManager(layoutManager); - - backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); - - fetchAllEvents(); - } - - private void fetchAllEvents() { - progressBar.setVisibility(View.VISIBLE); - - // Hent personlige hendelser (Nå med historikk) - List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - - RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (!isAdded()) return; - progressBar.setVisibility(View.GONE); - - List apiEvents = new ArrayList<>(); - if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); - apiEvents.add(e); - } - } - - // Flett og vis - List allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - - if (allEvents.isEmpty()) { - emptyView.setVisibility(View.VISIBLE); - recyclerView.setVisibility(View.GONE); - } else { - emptyView.setVisibility(View.GONE); - recyclerView.setVisibility(View.VISIBLE); - - CalendarAdapter adapter = new CalendarAdapter(allEvents, event -> { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - }); - recyclerView.setAdapter(adapter); - - // --- SCROLL TIL I DAG --- - scrollToToday(allEvents); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (!isAdded()) return; - progressBar.setVisibility(View.GONE); - - if (!deviceEvents.isEmpty()) { - CalendarAdapter adapter = new CalendarAdapter(deviceEvents, event -> { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - }); - recyclerView.setAdapter(adapter); - scrollToToday(deviceEvents); - } else { - emptyView.setText("Ingen hendelser funnet."); - emptyView.setVisibility(View.VISIBLE); - } - } - }); - } - - private void scrollToToday(List events) { - String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - int scrollIndex = 0; - - // Finn første event som er i dag eller senere - for (int i = 0; i < events.size(); i++) { - String raw = events.get(i).getRawDate(); - if (raw != null && raw.compareTo(today) >= 0) { - scrollIndex = i; - break; - } - } - - // Scroll litt ned slik at "i dag" havner på toppen, men ikke helt (offset 0) - // Bruker scrollToPositionWithOffset for presisjon - if (scrollIndex > 0) { - layoutManager.scrollToPositionWithOffset(scrollIndex, 0); - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarManager.java -============================================================ -package com.kbs.kbsintranett; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.provider.CalendarContract; -import androidx.core.content.ContextCompat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -public class CalendarManager { - - // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no - private static List getKbsCalendarIds(Context context) { - List ids = new ArrayList<>(); - - String[] projection = new String[] { - CalendarContract.Calendars._ID, - CalendarContract.Calendars.ACCOUNT_NAME - }; - - // Vi ser etter kontoer som slutter på @kbs.no - // (SQL: account_name LIKE '%@kbs.no') - String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; - String[] selectionArgs = new String[] {"%@kbs.no"}; - - try (Cursor cursor = context.getContentResolver().query( - CalendarContract.Calendars.CONTENT_URI, - projection, - selection, - selectionArgs, - null - )) { - if (cursor != null) { - while (cursor.moveToNext()) { - ids.add(cursor.getString(0)); // Legg til kalender-ID - } - } - } catch (Exception e) { - e.printStackTrace(); - } - return ids; - } - - public static List getDeviceEvents(Context context) { - List deviceEvents = new ArrayList<>(); - - if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) - != PackageManager.PERMISSION_GRANTED) { - return deviceEvents; - } - - // 1. Finn først ID-ene til KBS-kalenderne - List kbsCalendarIds = getKbsCalendarIds(context); - - // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste - if (kbsCalendarIds.isEmpty()) { - return deviceEvents; - } - - // Hent events fra 1 år tilbake og 1 år frem - long now = System.currentTimeMillis(); - long startMillis = now - (365L * 24 * 60 * 60 * 1000); - long endMillis = now + (365L * 24 * 60 * 60 * 1000); - - String[] projection = new String[]{ - CalendarContract.Events.TITLE, - CalendarContract.Events.DTSTART, - CalendarContract.Events.DTEND, - CalendarContract.Events.DESCRIPTION, - CalendarContract.Events.EVENT_LOCATION, - CalendarContract.Events.ALL_DAY - }; - - // 2. Bygg opp spørringen for å filtrere på disse ID-ene - // Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?" - StringBuilder selection = new StringBuilder("("); - List selectionArgsList = new ArrayList<>(); - - for (int i = 0; i < kbsCalendarIds.size(); i++) { - selection.append(CalendarContract.Events.CALENDAR_ID).append(" = ?"); - selectionArgsList.add(kbsCalendarIds.get(i)); - if (i < kbsCalendarIds.size() - 1) { - selection.append(" OR "); - } - } - selection.append(") AND "); - - selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); - selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); - - selectionArgsList.add(String.valueOf(startMillis)); - selectionArgsList.add(String.valueOf(endMillis)); - - String[] selectionArgs = selectionArgsList.toArray(new String[0]); - - try (Cursor cursor = context.getContentResolver().query( - CalendarContract.Events.CONTENT_URI, - projection, - selection.toString(), - selectionArgs, - CalendarContract.Events.DTSTART + " ASC" - )) { - if (cursor != null) { - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - - while (cursor.moveToNext()) { - String title = cursor.getString(0); - long dtStart = cursor.getLong(1); - long dtEnd = cursor.getLong(2); - String desc = cursor.getString(3); - String loc = cursor.getString(4); - int allDay = cursor.getInt(5); - - String rawStart; - String rawEnd; - - if (allDay == 1) { - SimpleDateFormat shortFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - rawStart = shortFormat.format(new Date(dtStart)); - rawEnd = shortFormat.format(new Date(dtEnd)); - } else { - rawStart = isoFormat.format(new Date(dtStart)); - rawEnd = isoFormat.format(new Date(dtEnd)); - } - - CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); - formatEventForUI(event); - deviceEvents.add(event); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - return deviceEvents; - } - - // --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) --- - public static void formatEventForUI(CalendarEvent event) { - if (event.getRawDate() == null) return; - - SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); - SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); - SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); - - outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - try { - Date date = null; - Date endDate = null; - boolean isAllDay = false; - - String raw = event.getRawDate(); - - if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { - SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - date = shortFmt.parse(raw); - isAllDay = true; - if (event.getRawEndDate() != null && event.getRawEndDate().length() == 10) { - endDate = shortFmt.parse(event.getRawEndDate()); - } - } - else if (raw.contains("T")) { - SimpleDateFormat isoFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - date = isoFmt.parse(raw); - if (event.getRawEndDate() != null && event.getRawEndDate().contains("T")) { - endDate = isoFmt.parse(event.getRawEndDate()); - } - } - else { - SimpleDateFormat sqlFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - sqlFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - date = sqlFmt.parse(raw); - if (event.getRawEndDate() != null) { - endDate = sqlFmt.parse(event.getRawEndDate()); - } - } - - if (date != null) { - event.setDay(outputDay.format(date)); - event.setMonth(outputMonth.format(date).toUpperCase()); - - if (isAllDay) { - event.setTime("Hele dagen"); - } else { - String timeStr = outputTime.format(date); - if (endDate != null) { - timeStr += " - " + outputTime.format(endDate); - } - event.setTime("Kl. " + timeStr); - } - } - - } catch (Exception e) { - e.printStackTrace(); - event.setDay("??"); - event.setMonth("???"); - event.setTime("Feil dato"); - } - } - - public static List mergeAndSort(List apiEvents, List deviceEvents) { - List all = new ArrayList<>(apiEvents); - all.addAll(deviceEvents); - - Collections.sort(all, (e1, e2) -> { - String d1 = e1.getRawDate() != null ? e1.getRawDate() : ""; - String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; - return d1.compareTo(d2); - }); - - return all; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\CategoryAdapter.java -============================================================ -package com.kbs.kbsintranett; - -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; -import java.util.List; - -public class CategoryAdapter extends RecyclerView.Adapter { - - private List categories; - private String selectedCategory = "Alle"; // Standardvalg - private OnCategoryClickListener listener; - - public interface OnCategoryClickListener { - void onCategoryClick(String category); - } - - public CategoryAdapter(List categories, OnCategoryClickListener listener) { - this.categories = categories; - this.listener = listener; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - String category = categories.get(position); - holder.name.setText(category); - - if (category.equals(selectedCategory)) { - // Valgt stil (Blå bakgrunn, hvit tekst) - holder.name.setBackgroundResource(R.drawable.bg_category_selected); - holder.name.setTextColor(Color.WHITE); - } else { - // Ikke valgt stil (Hvit bakgrunn, mørk tekst) - holder.name.setBackgroundResource(R.drawable.bg_category_unselected); - holder.name.setTextColor(Color.parseColor("#333333")); - } - - holder.itemView.setOnClickListener(v -> { - selectedCategory = category; - notifyDataSetChanged(); // Oppdater alle for å flytte markering - listener.onCategoryClick(category); - }); - } - - @Override - public int getItemCount() { - return categories.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - TextView name; - public ViewHolder(View view) { - super(view); - name = view.findViewById(R.id.category_name); - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\ChoicesAdapter.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -public class ChoicesAdapter implements JsonDeserializer> { - @Override - public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - // Hvis feltet er "null" eller en tom tekststreng, returner en tom liste - if (json.isJsonNull() || (json.isJsonPrimitive() && json.getAsString().isEmpty())) { - return new ArrayList<>(); - } - - // Hvis det faktisk er en liste (Array), les den som vanlig - if (json.isJsonArray()) { - List list = new ArrayList<>(); - for (JsonElement e : json.getAsJsonArray()) { - list.add(context.deserialize(e, GravityField.Choice.class)); - } - return list; - } - - // Hvis vi får noe annet rart (f.eks. en tekst som ikke er tom), ignorer det for å unngå krasj - return new ArrayList<>(); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\ConditionalLogicAdapter.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import java.lang.reflect.Type; - -public class ConditionalLogicAdapter implements JsonDeserializer { - @Override - public GravityField.ConditionalLogic deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - // Hvis feltet er en streng (f.eks tom streng ""), returner null - if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { - return null; - } - - // Hvis det er et objekt, bruk standard deserialisering - if (json.isJsonObject()) { - // Vi må manuelt deserialisere for å unngå uendelig løkke hvis vi bare kaller context.deserialize på samme type - // Enkleste måte er å la Gson gjøre jobben på innholdet - return new com.google.gson.Gson().fromJson(json, GravityField.ConditionalLogic.class); - } - - return null; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.Manifest; -import android.animation.LayoutTransition; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.DatePickerDialog; -import android.app.TimePickerDialog; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.provider.OpenableColumns; -import android.text.Editable; -import android.text.Html; -import android.text.InputType; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.ScrollView; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.fragment.app.Fragment; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonArray; -import com.google.gson.JsonParser; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okio.BufferedSink; -import okio.Okio; -import okio.Source; - -public class FormsFragment extends Fragment { - - private static final String TAG = "FormsFragment"; - private static final String BASE_URL_GF = "https://intranet.kbs.no/wp-json/gf/v2"; - - // SKJEMA ID-er - private static final int ID_ANSATTEOPPLYSNINGER = 1; - private static final int ID_RUH = 4; - private static final int ID_SIKKERHETSKURS = 9; - private static final int ID_HMS_BEKREFTELSE = 10; - private static final int ID_EGENMELDING = 11; - private static final int ID_SJA = 14; - private static final int ID_FRAVARSVARSEL = 15; - private static final int ID_REFUSJON_UTLEGG = 16; - - private int formId = 1; - - private LinearLayout formContainer; - private LinearLayout historyContainer; - private View historyWrapper; // Wrapper for historikk-modulen - private TextView txtStatus; - private TextView lblHistory; - private ProgressBar loadingSpinner; - private ImageView btnToggleHistory; - - // --- HOVEDSKJEMA STATE --- - private Map fieldWrappers = new HashMap<>(); - private Map inputViews = new HashMap<>(); - private Map requiredFieldsMap = new HashMap<>(); - private Map fileUploads = new HashMap<>(); - - // --- NESTED FORM (BARN) STATE --- - private Map childInputViews = new HashMap<>(); - private Map childRequiredFieldsMap = new HashMap<>(); - private Map childFileUploads = new HashMap<>(); - // Lagring av Nested Entries - private List nestedEntries = new ArrayList<>(); - private LinearLayout nestedEntriesContainer; - private TextView totalAmountView; - - // --- FILOPPLASTING & KAMERA --- - private String pendingFileFieldId = null; - private boolean isSelectingForChild = false; - private Uri currentPhotoUri = null; - - private ActivityResultLauncher filePickerLauncher; - private ActivityResultLauncher takePictureLauncher; - private ActivityResultLauncher requestPermissionLauncher; - - private GravityForm currentForm; - private final OkHttpClient client = new OkHttpClient(); - - private static final Pattern TITLE_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)"); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - filePickerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - Uri uri = result.getData().getData(); - if (uri != null && pendingFileFieldId != null) { - handleFileSelection(pendingFileFieldId, uri, isSelectingForChild); - } - } - } - ); - takePictureLauncher = registerForActivityResult( - new ActivityResultContracts.TakePicture(), - success -> { - if (success && currentPhotoUri != null && pendingFileFieldId != null) { - handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild); - } else if (!success) { - currentPhotoUri = null; - } - } - ); - requestPermissionLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), - isGranted -> { - if (isGranted) { - openCamera(); - } else { - Toast.makeText(getContext(), "Kameratillatelse er påkrevd for å ta bilde.", Toast.LENGTH_LONG).show(); - } - } - ); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_forms, container, false); - formContainer = view.findViewById(R.id.form_container); - historyContainer = view.findViewById(R.id.historyContainer); - historyWrapper = view.findViewById(R.id.history_wrapper); - txtStatus = view.findViewById(R.id.txt_status); - lblHistory = view.findViewById(R.id.lbl_history); - loadingSpinner = view.findViewById(R.id.loading_spinner); - - btnToggleHistory = view.findViewById(R.id.btn_toggle_history); - if (btnToggleHistory != null) { - btnToggleHistory.setOnClickListener(v -> toggleHistoryVisibility()); - } - - // --- FIKS FOR NULLPOINTER EXCEPTION PÅ LAYOUTTRANSITION --- - if (view instanceof ViewGroup) { - LayoutTransition transition = ((ViewGroup) view).getLayoutTransition(); - if (transition == null) { - transition = new LayoutTransition(); - ((ViewGroup) view).setLayoutTransition(transition); - } - transition.enableTransitionType(LayoutTransition.CHANGING); - } - // ---------------------------------------------------------- - - if (formContainer == null) { - formContainer = new LinearLayout(getContext()); - } - - if (getArguments() != null) { - int argId = getArguments().getInt("formId", 0); - if (argId != 0) formId = argId; - } - - fetchFormStructure(); - - return view; - } - - // --- UI LOGIKK FOR DELT SKJERM --- - - private void expandFormModule() { - if (historyWrapper != null && historyWrapper.getVisibility() == View.VISIBLE) { - historyWrapper.setVisibility(View.GONE); - if (btnToggleHistory != null) { - btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float); - } - } - } - - private void toggleHistoryVisibility() { - if (historyWrapper == null || btnToggleHistory == null) return; - - if (historyWrapper.getVisibility() == View.VISIBLE) { - // Skjul historikk - historyWrapper.setVisibility(View.GONE); - btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float); - } else { - // Vis historikk - historyWrapper.setVisibility(View.VISIBLE); - btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float); - } - } - - private void attachInteractionListener(View view) { - if (view == null) return; - view.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - expandFormModule(); - } - return false; - }); - view.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - expandFormModule(); - } - }); - if (view.isClickable()) { - view.setOnClickListener(v -> expandFormModule()); - } - } - - // ---------------------------------- - - private void fetchFormStructure() { - if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE); - updateStatus("Laster skjema..."); - - WordPressApiService api = RetrofitClient.getApiService(); - api.getForm(formId).enqueue(new retrofit2.Callback() { - @Override - public void onResponse(retrofit2.Call call, retrofit2.Response response) { - if (response.isSuccessful() && response.body() != null) { - currentForm = response.body(); - if (getActivity() != null) { - getActivity().runOnUiThread(() -> { - if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); - renderDynamicForm(currentForm); - fetchFormEntries(); - }); - } - } else { - updateStatus("Feil ved lasting av skjema."); - if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); - } - } - - @Override - public void onFailure(retrofit2.Call call, Throwable t) { - updateStatus("Nettverksfeil (Skjema): " + t.getMessage()); - if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); - } - }); - } - - private void renderDynamicForm(GravityForm form) { - if (formContainer == null) return; - formContainer.removeAllViews(); - fieldWrappers.clear(); - inputViews.clear(); - requiredFieldsMap.clear(); - fileUploads.clear(); - nestedEntries.clear(); - updateStatus(""); - - // Reset visibility of history on new load - if (historyWrapper != null) { - historyWrapper.setVisibility(View.VISIBLE); - if (btnToggleHistory != null) btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float); - } - - TextView title = new TextView(getContext()); - title.setText(getCleanTitle(form.title)); - title.setTextSize(24); - title.setTypeface(null, Typeface.BOLD); - title.setTextColor(Color.BLACK); - title.setPadding(0, 0, 0, 20); - formContainer.addView(title); - - if (form.description != null && !form.description.isEmpty()) { - TextView formDesc = new TextView(getContext()); - String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", ""); - formDesc.setText(cleanDesc); - formDesc.setPadding(0, 0, 0, 40); - formContainer.addView(formDesc); - } - - if (form.fields == null) return; - for (GravityField field : form.fields) { - if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) { - continue; - } - - LinearLayout fieldWrapper = new LinearLayout(getContext()); - fieldWrapper.setOrientation(LinearLayout.VERTICAL); - fieldWrapper.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - fieldWrapper.setPadding(0, 10, 0, 20); - - fieldWrappers.put(String.valueOf(field.id), fieldWrapper); - - if ("section".equals(field.type)) { - addSectionHeader(fieldWrapper, field.label, field.description); - formContainer.addView(fieldWrapper); - continue; - } - - if ("html".equals(field.type)) { - if (field.content != null && !field.content.isEmpty()) { - TextView htmlView = new TextView(getContext()); - htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT)); - fieldWrapper.addView(htmlView); - } - formContainer.addView(fieldWrapper); - continue; - } - - TextView label = new TextView(getContext()); - String labelText = field.label; - if (field.isRequired) labelText += " *"; - label.setText(labelText); - label.setTextColor(Color.DKGRAY); - label.setTypeface(null, Typeface.BOLD); - label.setPadding(0, 10, 0, 5); - fieldWrapper.addView(label); - - if ("form".equals(field.type)) { - renderNestedFormField(fieldWrapper, field); - } - else if ("product".equals(field.type) && "calculation".equals(field.inputType)) { - renderTotalSumField(fieldWrapper, field); - } - else if ("time".equals(field.type)) { - renderTimeField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("fileupload".equals(field.type)) { - renderFileUploadField(fieldWrapper, field, inputViews, requiredFieldsMap, false); - } else if (field.inputs != null && !field.inputs.isEmpty()) { - if ("consent".equals(field.type)) { - renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("checkbox".equals(field.type) || "multi_choice".equals(field.type)) { - renderCheckboxField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else { - renderCompositeField(fieldWrapper, field, inputViews, requiredFieldsMap); - } - } else if ("radio".equals(field.type)) { - renderRadioField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("select".equals(field.type)) { - renderSelectField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("textarea".equals(field.type)) { - renderTextAreaField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("date".equals(field.type)) { - renderDateField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else if ("consent".equals(field.type)) { - renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap); - } else { - renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap); - } - - if (field.description != null && !field.description.isEmpty()) { - TextView desc = new TextView(getContext()); - desc.setText(Html.fromHtml(field.description, Html.FROM_HTML_MODE_COMPACT)); - desc.setTextSize(12); - desc.setTextColor(Color.GRAY); - fieldWrapper.addView(desc); - } - - formContainer.addView(fieldWrapper); - } - - Button dynamicSubmit = new Button(getContext()); - dynamicSubmit.setText("Send inn skjema"); - dynamicSubmit.setTextColor(Color.WHITE); - dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(0, 60, 0, 20); - dynamicSubmit.setLayoutParams(params); - dynamicSubmit.setOnClickListener(v -> submitDynamicForm()); - formContainer.addView(dynamicSubmit); - - evaluateAllConditionalLogic(); - } - - // --- NESTED FORM LOGIKK --- - - private void renderNestedFormField(LinearLayout container, GravityField field) { - nestedEntriesContainer = new LinearLayout(getContext()); - nestedEntriesContainer.setOrientation(LinearLayout.VERTICAL); - nestedEntriesContainer.setPadding(0, 10, 0, 10); - container.addView(nestedEntriesContainer); - - Button btnAdd = new Button(getContext()); - btnAdd.setText("Legg til vedlegg"); - btnAdd.setBackgroundColor(Color.parseColor("#53AFE9")); - btnAdd.setTextColor(Color.WHITE); - btnAdd.setOnClickListener(v -> { - expandFormModule(); // Trigger expand - int childFormId = 18; - if (field.gpnfForm != null) { - try { - childFormId = Integer.parseInt(field.gpnfForm); - } catch (NumberFormatException e) { e.printStackTrace(); } - } - // NYTT: Sender med felt-ID fra hovedskjemaet (f.eks "25") - openChildFormDialog(childFormId, field.id); - }); - container.addView(btnAdd); - - EditText hiddenIds = new EditText(getContext()); - hiddenIds.setVisibility(View.GONE); - inputViews.put(field.id, hiddenIds); - } - - private void renderTotalSumField(LinearLayout container, GravityField field) { - totalAmountView = new TextView(getContext()); - totalAmountView.setText("Kr 0,00"); - totalAmountView.setTextSize(18); - totalAmountView.setTypeface(null, Typeface.BOLD); - totalAmountView.setPadding(10, 10, 10, 10); - container.addView(totalAmountView); - } - - // NY SIGNATUR: Tar imot parentFieldId - private void openChildFormDialog(int childFormId, String parentFieldId) { - if (getActivity() == null) return; - ProgressBar pBar = new ProgressBar(getContext()); - AlertDialog loadingDialog = new AlertDialog.Builder(getContext()) - .setView(pBar) - .setMessage("Laster skjema...") - .setCancelable(false) - .show(); - RetrofitClient.getApiService().getForm(childFormId).enqueue(new retrofit2.Callback() { - @Override - public void onResponse(retrofit2.Call call, retrofit2.Response response) { - loadingDialog.dismiss(); - if (response.isSuccessful() && response.body() != null) { - showChildFormDialog(response.body(), parentFieldId); - } else { - Toast.makeText(getContext(), "Kunne ikke hente underskjema", Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onFailure(retrofit2.Call call, Throwable t) { - loadingDialog.dismiss(); - Toast.makeText(getContext(), "Feil: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); - } - - // NY SIGNATUR: Tar imot parentFieldId - private void showChildFormDialog(GravityForm childForm, String parentFieldId) { - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - childInputViews.clear(); - childRequiredFieldsMap.clear(); - childFileUploads.clear(); - - ScrollView scrollView = new ScrollView(getContext()); - LinearLayout layout = new LinearLayout(getContext()); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setPadding(30, 30, 30, 30); - scrollView.addView(layout); - for (GravityField field : childForm.fields) { - if ("hidden".equals(field.type) || field.isHidden) continue; - LinearLayout wrapper = new LinearLayout(getContext()); - wrapper.setOrientation(LinearLayout.VERTICAL); - wrapper.setPadding(0, 10, 0, 20); - - TextView label = new TextView(getContext()); - String lText = field.label; - if (field.isRequired) lText += " *"; - label.setText(lText); - label.setTypeface(null, Typeface.BOLD); - wrapper.addView(label); - if ("fileupload".equals(field.type)) { - renderFileUploadField(wrapper, field, childInputViews, childRequiredFieldsMap, true); - } else if ("product".equals(field.type)) { - EditText input = new EditText(getContext()); - input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - input.setHint("Kr 0.00"); - wrapper.addView(input); - childInputViews.put(field.id, input); - childRequiredFieldsMap.put(field.id, field.isRequired); - } else { - renderTextField(wrapper, field, childInputViews, childRequiredFieldsMap); - } - layout.addView(wrapper); - } - - builder.setView(scrollView); - builder.setPositiveButton("Legg til vedlegg", null); - builder.setNegativeButton("Avbryt", (d, w) -> d.dismiss()); - AlertDialog dialog = builder.create(); - dialog.show(); - - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { - submitChildForm(childForm.id, dialog, parentFieldId); - }); - } - - // NY SIGNATUR: Tar imot parentFieldId og sender den som meta - private void submitChildForm(int childFormId, AlertDialog dialog, String parentFieldId) { - JSONObject inputValues = new JSONObject(); - for (Map.Entry entry : childInputViews.entrySet()) { - String val = getInputValueGeneric(entry.getValue()); - if (!val.isEmpty()) { - try { - inputValues.put("input_" + entry.getKey(), val); - } catch (JSONException e) { e.printStackTrace(); } - } - } - - if (!childFileUploads.isEmpty()) { - List fileParts = new ArrayList<>(); - Map textParts = new HashMap<>(); - - try { - JSONArray names = inputValues.names(); - if (names != null) { - for (int i = 0; i < names.length(); i++) { - String key = names.getString(i); - String val = inputValues.getString(key); - textParts.put(key, RequestBody.create(MultipartBody.FORM, val)); - } - } - - // --- HER ER FIKSEN FOR BUGGEN --- - // Vi legger ved en ekstra parameter som forteller Gravity Forms at dette - // er en nested entry som hører til et bestemt felt (f.eks "25"). - if (parentFieldId != null) { - textParts.put("gpnf_entry_nested_form_field", RequestBody.create(MultipartBody.FORM, parentFieldId)); - } - // --------------------------------- - - for (Map.Entry fileEntry : childFileUploads.entrySet()) { - String fieldId = fileEntry.getKey(); - Uri uri = fileEntry.getValue(); - if (uri != null) { - MultipartBody.Part part = getFilePart("input_" + fieldId, uri); - if (part != null) fileParts.add(part); - } - } - - Toast.makeText(getContext(), "Laster opp vedlegg...", Toast.LENGTH_SHORT).show(); - RetrofitClient.getApiService().submitMultipartForm(childFormId, textParts, fileParts).enqueue(new retrofit2.Callback() { - @Override - public void onResponse(retrofit2.Call call, retrofit2.Response response) { - if (response.isSuccessful() && response.body() != null) { - try { - JsonObject json = response.body().getAsJsonObject(); - if (json.has("is_valid") && json.get("is_valid").getAsBoolean()) { - String entryId = json.has("entry_id") ? json.get("entry_id").getAsString() : ""; - - // NB: Tilpass ID-ene her hvis skjema 18 endres. - // ID 3 = Beskrivelse, ID 4 = Beløp - String desc = getInputValueGeneric(childInputViews.get("3")); - String price = getInputValueGeneric(childInputViews.get("4")); - addNestedEntry(entryId, desc, price); - dialog.dismiss(); - } else { - Toast.makeText(getContext(), "Ugyldig respons fra server", Toast.LENGTH_SHORT).show(); - } - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(getContext(), "Feil ved parsing av svar", Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(getContext(), "Feil ved opplasting", Toast.LENGTH_SHORT).show(); - } - } - @Override - public void onFailure(retrofit2.Call call, Throwable t) { - Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); - } - }); - } catch (Exception e) { e.printStackTrace(); } - } - } - - private void addNestedEntry(String entryId, String description, String price) { - nestedEntries.add(new NestedEntry(entryId, description, price)); - refreshNestedList(); - } - - private void refreshNestedList() { - if (nestedEntriesContainer == null) return; - nestedEntriesContainer.removeAllViews(); - - double total = 0; - List ids = new ArrayList<>(); - for (NestedEntry entry : nestedEntries) { - ids.add(entry.id); - String cleanPrice = entry.price.replaceAll("[^0-9,.]", "").replace(",", "."); - try { - if (!cleanPrice.isEmpty()) total += Double.parseDouble(cleanPrice); - } catch (NumberFormatException e) { } - - LinearLayout row = new LinearLayout(getContext()); - row.setOrientation(LinearLayout.HORIZONTAL); - row.setPadding(10, 10, 10, 10); - - TextView txt = new TextView(getContext()); - txt.setText(entry.description + " (" + entry.price + ")"); - txt.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); - - row.addView(txt); - nestedEntriesContainer.addView(row); - } - - if (totalAmountView != null) { - totalAmountView.setText("Totalt: Kr " + String.format("%.2f", total)); - } - - View hiddenField = inputViews.get("25"); - if (hiddenField instanceof EditText) { - ((EditText)hiddenField).setText(TextUtils.join(",", ids)); - } - } - - // --- FELLES METODER (FILE UPLOAD M/ CAMERA STØTTE) --- - - private void renderFileUploadField(LinearLayout container, GravityField field, Map viewsMap, Map reqMap, boolean isChild) { - LinearLayout fileLayout = new LinearLayout(getContext()); - fileLayout.setOrientation(LinearLayout.HORIZONTAL); - - Button btnUpload = new Button(getContext()); - btnUpload.setText("Velg fil / Ta bilde"); - btnUpload.setOnClickListener(v -> { - // Setter state før vi viser dialog - pendingFileFieldId = field.id; - isSelectingForChild = isChild; - expandFormModule(); // Trigger expand - showFileSourceDialog(); - }); - TextView txtFileName = new TextView(getContext()); - txtFileName.setText("Ingen fil valgt"); - txtFileName.setPadding(20, 0, 0, 0); - txtFileName.setTextColor(Color.GRAY); - btnUpload.setTag(txtFileName); - - fileLayout.addView(btnUpload); - fileLayout.addView(txtFileName); - container.addView(fileLayout); - - viewsMap.put(field.id, btnUpload); - reqMap.put(field.id, field.isRequired); - } - - // Hjelpemetode for å vise dialog - private void showFileSourceDialog() { - String[] options = {"Ta bilde", "Velg fil"}; - new AlertDialog.Builder(getContext()) - .setTitle("Last opp vedlegg") - .setItems(options, (dialog, which) -> { - if (which == 0) { - // Ta bilde - SJEKKER PERMISSION FØRST - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED) { - openCamera(); - } else { - // Spør om lov - requestPermissionLauncher.launch(Manifest.permission.CAMERA); - } - } else { - // Velg fil - if (filePickerLauncher != null) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - String[] mimeTypes = {"image/jpeg", "image/png", "application/pdf"}; - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); - filePickerLauncher.launch(intent); - } - } - }) - .show(); - } - - private void openCamera() { - currentPhotoUri = createImageUri(); - if (currentPhotoUri != null && takePictureLauncher != null) { - try { - takePictureLauncher.launch(currentPhotoUri); - } catch (Exception e) { - Toast.makeText(getContext(), "Kunne ikke starte kamera: " + e.getMessage(), Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Camera launch failed", e); - } - } else { - Toast.makeText(getContext(), "Kunne ikke opprette bildefil", Toast.LENGTH_SHORT).show(); - } - } - - private Uri createImageUri() { - try { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); - String imageFileName = "JPEG_" + timeStamp + "_"; - File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES); - File image = File.createTempFile(imageFileName, ".jpg", storageDir); - return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - private void handleFileSelection(String fieldId, Uri uri, boolean isChild) { - if (isChild) { - childFileUploads.put(fieldId, uri); - } else { - fileUploads.put(fieldId, uri); - } - - Map targetMap = isChild ? childInputViews : inputViews; - View view = targetMap.get(fieldId); - if (view instanceof Button) { - TextView txtView = (TextView) view.getTag(); - if (txtView != null) { - txtView.setText(getFileName(uri)); - txtView.setTextColor(Color.BLACK); - } - } - } - - private String getFileName(Uri uri) { - String result = null; - if (uri.getScheme().equals("content")) { - try (Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - if(index >= 0) result = cursor.getString(index); - } - } catch (Exception e) {} - } - if (result == null) { - result = uri.getPath(); - int cut = result.lastIndexOf('/'); - if (cut != -1) result = result.substring(cut + 1); - } - return result; - } - - private String getCleanTitle(String title) { - if (title == null) return ""; - Matcher m = TITLE_PATTERN.matcher(title.trim()); - if (m.find()) { - return m.group(2); - } - return title; - } - - // --- STANDARD RENDER METODER --- - - private void renderTimeField(LinearLayout container, GravityField field, Map views, Map req) { - EditText timeInput = new EditText(getContext()); - timeInput.setFocusable(false); - timeInput.setClickable(true); - timeInput.setHint("00:00"); - timeInput.setOnClickListener(v -> { - expandFormModule(); // Trigger expand - Calendar mcurrentTime = Calendar.getInstance(); - int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY); - int minute = mcurrentTime.get(Calendar.MINUTE); - new TimePickerDialog(getContext(), (timePicker, selectedHour, selectedMinute) -> { - timeInput.setText(String.format("%02d:%02d", selectedHour, selectedMinute)); - evaluateAllConditionalLogic(); - }, hour, minute, true).show(); - }); - container.addView(timeInput); - views.put(field.id, timeInput); - req.put(field.id, field.isRequired); - } - - private void renderTextField(LinearLayout container, GravityField field, Map views, Map req) { - EditText input = new EditText(getContext()); - input.setPadding(30, 30, 30, 30); - input.setBackgroundResource(android.R.drawable.edit_text); - - if ("number".equals(field.type) || "phone".equals(field.type)) { - input.setInputType(InputType.TYPE_CLASS_PHONE); - } else if ("email".equals(field.type)) { - input.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - } else { - input.setInputType(InputType.TYPE_CLASS_TEXT); - } - - if (views == inputViews) { - UserManager user = UserManager.getInstance(); - String lowerLabel = field.label.toLowerCase(); - if (lowerLabel.contains("e-post")) input.setText(user.getUserEmail()); - if (lowerLabel.contains("navn") || lowerLabel.contains("melder")) input.setText(user.getUserDisplayName()); - if (lowerLabel.contains("stilling")) input.setText(user.getStilling()); - if (lowerLabel.contains("mobil")) input.setText(user.getMobiltelefon()); - } - - input.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - public void onTextChanged(CharSequence s, int start, int before, int count) {} - public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); } - }); - attachInteractionListener(input); // Add listener - - container.addView(input); - views.put(field.id, input); - req.put(field.id, field.isRequired); - } - - private void renderTextAreaField(LinearLayout container, GravityField field, Map views, Map req) { - EditText input = new EditText(getContext()); - input.setBackgroundResource(android.R.drawable.edit_text); - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - input.setMinLines(3); - input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START); - - attachInteractionListener(input); // Add listener - - container.addView(input); - views.put(field.id, input); - req.put(field.id, field.isRequired); - } - - private void renderRadioField(LinearLayout container, GravityField field, Map views, Map req) { - RadioGroup group = new RadioGroup(getContext()); - if (field.choices != null) { - for (GravityField.Choice choice : field.choices) { - RadioButton rb = new RadioButton(getContext()); - rb.setText(choice.text); - rb.setTag(choice.value); - // Also trigger expand on RadioButton click - rb.setOnClickListener(v -> { - expandFormModule(); - evaluateAllConditionalLogic(); - }); - group.addView(rb); - } - } - // Fallback listener - group.setOnCheckedChangeListener((g, i) -> { - expandFormModule(); - evaluateAllConditionalLogic(); - }); - container.addView(group); - views.put(field.id, group); - req.put(field.id, field.isRequired); - } - - private void renderSelectField(LinearLayout container, GravityField field, Map views, Map req) { - Spinner spinner = new Spinner(getContext()); - // Spinner touch listener is tricky, usually set onTouchListener works - spinner.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - expandFormModule(); - } - return false; - }); - List labels = new ArrayList<>(); - labels.add("- Velg -"); - if (field.choices != null) { - for (GravityField.Choice c : field.choices) labels.add(c.text); - } - ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, labels); - spinner.setAdapter(adapter); - container.addView(spinner); - views.put(field.id, spinner); - req.put(field.id, field.isRequired); - } - - private void renderConsentField(LinearLayout container, GravityField field, Map views, Map req) { - CheckBox checkBox = new CheckBox(getContext()); - String cbText = (field.checkboxLabel != null && !field.checkboxLabel.isEmpty()) ? field.checkboxLabel : field.label; - checkBox.setText(cbText); - String inputId = (field.inputs != null && !field.inputs.isEmpty()) ? field.inputs.get(0).id : field.id; - - checkBox.setTag("1"); - checkBox.setOnCheckedChangeListener((b, c) -> { - expandFormModule(); - evaluateAllConditionalLogic(); - }); - container.addView(checkBox); - views.put(inputId, checkBox); - req.put(inputId, field.isRequired); - } - - private void renderCheckboxField(LinearLayout container, GravityField field, Map views, Map req) { - if (field.inputs != null) { - for (int i = 0; i < field.inputs.size(); i++) { - GravityField inputDef = field.inputs.get(i); - CheckBox checkBox = new CheckBox(getContext()); - checkBox.setText(inputDef.label); - - String value = "1"; - // Fallback - if (field.choices != null && i < field.choices.size()) { - value = field.choices.get(i).value; - } - checkBox.setTag(value); - checkBox.setOnCheckedChangeListener((b, c) -> { - expandFormModule(); - evaluateAllConditionalLogic(); - }); - container.addView(checkBox); - views.put(inputDef.id, checkBox); - req.put(inputDef.id, false); - } - } - } - - private void renderDateField(LinearLayout container, GravityField field, Map views, Map req) { - EditText dateInput = new EditText(getContext()); - // --- DATO LØSNING (Fix for Read Only / Dagens Dato) --- - if (field.readOnly || (formId == ID_REFUSJON_UTLEGG && "28".equals(field.id))) { - // Sett dagens dato - SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); - dateInput.setText(df.format(new Date())); - - // Gjør den "read-only" men synlig - dateInput.setFocusable(false); - dateInput.setClickable(false); - dateInput.setEnabled(false); - dateInput.setTextColor(Color.BLACK); - } else { - // Vanlig dato-velger - dateInput.setFocusable(false); - dateInput.setClickable(true); - dateInput.setHint("dd.mm.yyyy"); - dateInput.setOnClickListener(v -> { - expandFormModule(); // Trigger expand - Calendar c = Calendar.getInstance(); - new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> { - dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year)); - evaluateAllConditionalLogic(); - }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show(); - }); - } - - dateInput.setPadding(30, 30, 30, 30); - dateInput.setBackgroundResource(android.R.drawable.edit_text); - - container.addView(dateInput); - views.put(field.id, dateInput); - req.put(field.id, field.isRequired); - } - - private void renderCompositeField(LinearLayout container, GravityField parentField, Map views, Map req) { - UserManager user = UserManager.getInstance(); - boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER); - - List inputs = new ArrayList<>(parentField.inputs); - if ("address".equals(parentField.type)) { - Collections.sort(inputs, (f1, f2) -> Integer.compare(getAddressScore(f1.label), getAddressScore(f2.label))); - } - - for (GravityField subField : inputs) { - if (subField.isHidden || "hidden".equals(subField.visibility)) continue; - TextView subLabel = new TextView(getContext()); - String subLabelText = subField.label; - boolean isSubRequired = parentField.isRequired; - if ("address".equals(parentField.type) && subField.id.endsWith(".2")) { - isSubRequired = false; - } - if (isSubRequired) subLabelText += " *"; - - subLabel.setText(subLabelText); - subLabel.setTextColor(Color.GRAY); - subLabel.setTextSize(12); - subLabel.setPadding(0, 10, 0, 0); - container.addView(subLabel); - - EditText subInput = new EditText(getContext()); - subInput.setPadding(30, 30, 30, 30); - subInput.setBackgroundResource(android.R.drawable.edit_text); - subInput.setInputType(InputType.TYPE_CLASS_TEXT); - if (isPersonalia && parentField.label.toLowerCase().contains("navn") && !parentField.label.toLowerCase().contains("pårørende")) { - String lowerSub = subField.label.toLowerCase(); - if (lowerSub.contains("fornavn")) subInput.setText(user.getFirstName()); - else if (lowerSub.contains("etternavn")) subInput.setText(user.getLastName()); - } - - attachInteractionListener(subInput); - container.addView(subInput); - views.put(subField.id, subInput); - req.put(subField.id, isSubRequired); - } - } - - private void addSectionHeader(LinearLayout container, String title, String descText) { - TextView sectionHeader = new TextView(getContext()); - sectionHeader.setText(title); - sectionHeader.setTextSize(18); - sectionHeader.setTypeface(null, Typeface.BOLD); - sectionHeader.setTextColor(Color.parseColor("#0069B3")); - sectionHeader.setPadding(0, 20, 0, 5); - container.addView(sectionHeader); - if (descText != null && !descText.isEmpty()) { - TextView desc = new TextView(getContext()); - desc.setText(Html.fromHtml(descText, Html.FROM_HTML_MODE_COMPACT)); - desc.setTextSize(12); - desc.setTextColor(Color.GRAY); - container.addView(desc); - } - - View line = new View(getContext()); - line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2)); - line.setBackgroundColor(Color.LTGRAY); - line.setPadding(0,0,0,20); - container.addView(line); - } - - private int getAddressScore(String label) { - if (label == null) return 99; - String l = label.toLowerCase(); - if (l.contains("adresselinje 1")) return 1; - if (l.contains("adresselinje 2")) return 2; - if (l.contains("postnummer") || l.contains("zip")) return 3; - if (l.contains("poststed") || l.contains("city")) return 4; - if (l.contains("land") || l.contains("country")) return 5; - return 99; - } - - private void evaluateAllConditionalLogic() { - if (currentForm == null || currentForm.fields == null) return; - for (GravityField field : currentForm.fields) { - if (field.conditionalLogic == null) { - setViewVisibility(field.id, true); - continue; - } - - boolean isMatch = evaluateLogic(field.conditionalLogic); - boolean show = "show".equalsIgnoreCase(field.conditionalLogic.actionType); - boolean shouldBeVisible = (show && isMatch) || (!show && !isMatch); - setViewVisibility(field.id, shouldBeVisible); - } - } - - private boolean evaluateLogic(GravityField.ConditionalLogic logic) { - if (logic.rules == null || logic.rules.isEmpty()) return true; - boolean isAll = "all".equalsIgnoreCase(logic.logicType); - boolean aggregatedResult = isAll; - - for (GravityField.Rule rule : logic.rules) { - String val = getInputValue(rule.fieldId); - boolean ruleMatch = checkRule(val, rule.operator, rule.value); - - if (isAll) { - aggregatedResult = aggregatedResult && ruleMatch; - if (!aggregatedResult) break; - } else { - aggregatedResult = aggregatedResult || - ruleMatch; - if (aggregatedResult) break; - } - } - return aggregatedResult; - } - - private boolean checkRule(String actualValue, String operator, String targetValue) { - if (actualValue == null) actualValue = ""; - if (targetValue == null) targetValue = ""; - - switch (operator.toLowerCase()) { - case "is": return actualValue.equalsIgnoreCase(targetValue); - case "isnot": return !actualValue.equalsIgnoreCase(targetValue); - case "contains": return actualValue.toLowerCase().contains(targetValue.toLowerCase()); - case "starts_with": return actualValue.toLowerCase().startsWith(targetValue.toLowerCase()); - case "ends_with": return actualValue.toLowerCase().endsWith(targetValue.toLowerCase()); - default: return false; - } - } - - private String getInputValue(String fieldId) { - View view = inputViews.get(fieldId); - return getInputValueGeneric(view); - } - - private String getInputValueGeneric(View view) { - if (view == null) return ""; - if (view instanceof EditText) return ((EditText) view).getText().toString(); - if (view instanceof RadioGroup) { - int id = ((RadioGroup) view).getCheckedRadioButtonId(); - if (id != -1) { - View rb = view.findViewById(id); - if (rb != null && rb.getTag() != null) return rb.getTag().toString(); - } - } - if (view instanceof Spinner) { - if (((Spinner) view).getSelectedItemPosition() == 0) return ""; - Object item = ((Spinner) view).getSelectedItem(); - return item != null ? item.toString() : ""; - } - if (view instanceof CheckBox) { - CheckBox cb = (CheckBox) view; - if (cb.isChecked()) { - return cb.getTag() != null ? - cb.getTag().toString() : "1"; - } - return ""; - } - return ""; - } - - private void setViewVisibility(String fieldId, boolean visible) { - View wrapper = fieldWrappers.get(fieldId); - if (wrapper != null) { - wrapper.setVisibility(visible ? View.VISIBLE : View.GONE); - } - } - - // --- SUBMISSION --- - - private void submitDynamicForm() { - JSONObject inputValues = new JSONObject(); - boolean hasValues = false; - - Log.d(TAG, "submitDynamicForm: Starting validation..."); - - for (Map.Entry entry : inputViews.entrySet()) { - String fieldId = entry.getKey(); - View view = entry.getValue(); - - View wrapper = fieldWrappers.get(fieldId); - if (wrapper == null) { - if (!view.isShown()) continue; - } else { - if (wrapper.getVisibility() != View.VISIBLE) continue; - } - - String val = getInputValueGeneric(view); - Boolean req = requiredFieldsMap.get(fieldId); - if (req != null && req && val.isEmpty() && !(view instanceof Button)) { - Log.d(TAG, "Validation failed for field " + fieldId); - if (view instanceof EditText) { - ((EditText)view).setError("Må fylles ut"); - view.requestFocus(); - } else { - Toast.makeText(getContext(), "Fyll ut alle felt", Toast.LENGTH_SHORT).show(); - } - return; - } - if (!val.isEmpty()) { - try { - GravityField fieldDef = getGravityFieldById(fieldId); - if (fieldDef != null && "date".equals(fieldDef.type)) { - val = formatDateForApi(val); - } - - inputValues.put("input_" + fieldId, val); - hasValues = true; - } catch (JSONException e) {} - } - } - - if (!hasValues && fileUploads.isEmpty()) { - Log.d(TAG, "Submit aborted: Form is empty"); - Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show(); - return; - } - - updateStatus("Sender inn..."); - String cookie = UserManager.getInstance().getCookie(); - Log.d(TAG, "Preparing submission payload: " + inputValues.toString()); - - if (!fileUploads.isEmpty()) { - Log.d(TAG, "Submitting as Multipart..."); - sendMultipart(inputValues); - } else { - Log.d(TAG, "Submitting as JSON..."); - RequestBody body = RequestBody.create(MediaType.parse("application/json"), inputValues.toString()); - String url = BASE_URL_GF + "/forms/" + formId + "/submissions"; - Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build(); - client.newCall(request).enqueue(new okhttp3.Callback() { - public void onFailure(okhttp3.Call call, IOException e) { - Log.e(TAG, "JSON submit failed", e); - updateStatus("Feil: " + e.getMessage()); - } - public void onResponse(okhttp3.Call call, Response response) { - Log.d(TAG, "JSON response code: " + response.code()); - if (response.isSuccessful()) { - if (getActivity() != null) getActivity().runOnUiThread(() -> { - Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); - fetchFormEntries(); - updateStatus("OK"); - clearInputs(); - }); - } else { - try { - String errBody = response.body() != null ? response.body().string() : "No body"; - Log.e(TAG, "Server error body: " + errBody); - updateStatus("Feil (" + response.code() + "): " + errBody); - } catch(Exception e){} - } - } - }); - } - } - - private String formatDateForApi(String dateStr) { - if (dateStr == null || dateStr.isEmpty()) return ""; - try { - SimpleDateFormat displayFormat = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); - Date date = displayFormat.parse(dateStr); - SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - return apiFormat.format(date); - } catch (Exception e) { - return dateStr; - } - } - - private GravityField getGravityFieldById(String id) { - if (currentForm == null || currentForm.fields == null) return null; - for (GravityField f : currentForm.fields) { - if (f.id.equals(id)) return f; - if (f.inputs != null) { - for (GravityField sub : f.inputs) { - if (sub.id.equals(id)) return sub; - } - } - } - return null; - } - - private void sendMultipart(JSONObject inputValues) { - List fileParts = new ArrayList<>(); - Map textParts = new HashMap<>(); - try { - JSONArray names = inputValues.names(); - if (names != null) { - for(int i=0; i entry : fileUploads.entrySet()) { - MultipartBody.Part part = getFilePart("input_" + entry.getKey(), entry.getValue()); - if (part != null) fileParts.add(part); - } - RetrofitClient.getApiService().submitMultipartForm(formId, textParts, fileParts).enqueue(new retrofit2.Callback() { - public void onResponse(retrofit2.Call call, retrofit2.Response response) { - if (response.isSuccessful()) { - if (getActivity() != null) getActivity().runOnUiThread(() -> { - Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); - fetchFormEntries(); - updateStatus("OK"); - clearInputs(); - }); - } else { - updateStatus("Feil: " + response.code()); - } - } - public void onFailure(retrofit2.Call call, Throwable t) { updateStatus("Feil: " + t.getMessage()); } - }); - } catch (Exception e) {} - } - - private MultipartBody.Part getFilePart(String partName, Uri uri) { - try { - InputStream inputStream = getContext().getContentResolver().openInputStream(uri); - String fileName = getFileName(uri); - RequestBody requestBody = new RequestBody() { - @Override public MediaType contentType() { return MediaType.parse("application/octet-stream"); - } - @Override public void writeTo(BufferedSink sink) throws IOException { - try (Source source = Okio.source(inputStream)) { sink.writeAll(source); - } - } - }; - return MultipartBody.Part.createFormData(partName, fileName, requestBody); - } catch (Exception e) { return null; - } - } - - private void fetchFormEntries() { - UserManager user = UserManager.getInstance(); - String cookie = user.getCookie(); - int userId = user.getUserId(); - - if (cookie == null) return; - String searchJson = "{\"field_filters\":[{\"key\":\"created_by\",\"value\":\"" + userId + "\"}]}"; - String encodedSearch = ""; - try { - encodedSearch = URLEncoder.encode(searchJson, "UTF-8"); - } catch (UnsupportedEncodingException e) { e.printStackTrace(); } - - String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch; - Request request = new Request.Builder().url(url).header("Cookie", cookie).build(); - - client.newCall(request).enqueue(new okhttp3.Callback() { - @Override - public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException e) { - Log.e(TAG, "Kunne ikke hente historikk", e); - } - - @Override - public void onResponse(@NonNull okhttp3.Call call, @NonNull Response response) throws IOException { - if (response.isSuccessful()) { - String jsonStr = response.body().string(); - try { - JSONObject json = new JSONObject(jsonStr); - if (json.has("entries")) { - JSONArray entries = json.getJSONArray("entries"); - if (getActivity() != null) { - getActivity().runOnUiThread(() -> { - showHistory(entries); - if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0) - { - try { - prefillFormFromHistory(entries.getJSONObject(0)); - } catch (JSONException e) { e.printStackTrace(); } - } - }); - } - } - } catch (JSONException e) { e.printStackTrace(); - } - } - } - }); - } - - private void showHistory(JSONArray entries) { - if (historyContainer == null) return; - historyContainer.removeAllViews(); - - if (entries.length() == 0) { - if (lblHistory != null) lblHistory.setVisibility(View.GONE); - return; - } else { - if (lblHistory != null) lblHistory.setVisibility(View.VISIBLE); - } - - try { - // Vis flere oppføringer siden vi nå har scrolle-mulighet øverst - int count = Math.min(entries.length(), 20); - for (int i = 0; i < count; i++) { - JSONObject entry = entries.getJSONObject(i); - String date = entry.optString("date_created"); - - // Prøv å finne en bedre tittel enn bare dato (f.eks Prosjektnavn eller Sted) - String titleText = "Innsendt: " + date; - TextView item = new TextView(getContext()); - item.setText(titleText); - item.setPadding(10, 20, 10, 20); - item.setBackgroundResource(android.R.drawable.list_selector_background); - item.setTextSize(14); - // Add click listener to show details - item.setOnClickListener(v -> showEntryDetails(entry)); - View line = new View(getContext()); - line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - line.setBackgroundColor(Color.LTGRAY); - - historyContainer.addView(item); - historyContainer.addView(line); - } - } catch (JSONException e) { e.printStackTrace(); - } - } - - // NY METODE: Vis detaljer i dialog med delingsknapp - private void showEntryDetails(JSONObject entry) { - // HVIS dette er Form 16 (Refusjon), må vi først hente vedleggene (som ligger i Form 18) - if (formId == ID_REFUSJON_UTLEGG) { - Log.d(TAG, "Form 16 detected. Checking for child entries..."); - String nestedIds = entry.optString("25"); // Felt 25 i Form 16 inneholder ID-ene til vedleggene - - if (!nestedIds.isEmpty()) { - Log.d(TAG, "Nested IDs found: " + nestedIds); - List ids = new ArrayList<>(); - - // ROBUST PARSING AV ID-LISTE - if (nestedIds.startsWith("[") && nestedIds.endsWith("]")) { - // Dette er en JSON-array streng (f.eks [101, 102]) - try { - JSONArray jsonArray = new JSONArray(nestedIds); - for(int i=0; iInnsendt: ").append(date).append("

"); - text.append("Innsendt: ").append(date).append("\n\n"); - - if (currentForm != null && currentForm.fields != null) { - for (GravityField field : currentForm.fields) { - if ("section".equals(field.type) || "html".equals(field.type) || "captcha".equals(field.type)) continue; - // Hopp over felt 25 (Vedleggs-IDer) i Form 16, siden vi viser det bedre senere - if (formId == ID_REFUSJON_UTLEGG && "25".equals(field.id)) continue; - - String value = ""; - if (field.inputs != null && !field.inputs.isEmpty()) { - for (GravityField input : field.inputs) { - String subVal = entry.optString(input.id); - if (!subVal.isEmpty()) value += " " + subVal; - } - } else { - value = entry.optString(String.valueOf(field.id)); - } - - if (!value.trim().isEmpty()) { - if ("fileupload".equals(field.type)) { - // Håndter filopplastinger (single/multiple) - // Her antar vi enkel URL, men for sikkerhets skyld bruker vi extractUrl - String cleanUrl = extractUrl(value); - if (cleanUrl.startsWith("http")) { - html.append("").append(field.label).append(":
") - .append("Åpne fil

"); - text.append(field.label).append(":\n").append(cleanUrl).append("\n\n"); - } else { - html.append("").append(field.label).append(":
").append(value).append("

"); - text.append(field.label).append(":\n").append(value).append("\n\n"); - } - } else { - html.append("").append(field.label).append(":
").append(value).append("

"); - text.append(field.label).append(":\n").append(value).append("\n\n"); - } - } - } - } - } catch (Exception e) {} - } - - // Rekursiv metode for å hente barn-oppføringer (Vedlegg) - private void fetchChildEntriesRecursive(List ids, int index, StringBuilder html, StringBuilder text, AlertDialog loader) { - if (index >= ids.size()) { - // Alle ferdige! Vis dialogen. - loader.dismiss(); - showFinalDialog(html, text); - return; - } - - String entryId = ids.get(index); - Log.d(TAG, "Fetching child entry ID: " + entryId); - - RetrofitClient.getApiService().getSingleEntry(entryId).enqueue(new retrofit2.Callback() { - @Override - public void onResponse(retrofit2.Call call, retrofit2.Response response) { - if (response.isSuccessful() && response.body() != null) { - try { - JsonObject json = response.body().getAsJsonObject(); - // I Form 18: Felt 1 = Fil, Felt 3 = Beskrivelse, Felt 4 = Beløp - - // Parse Description and Price - String desc = json.has("3") ? json.get("3").getAsString() : "Uten beskrivelse"; - String price = json.has("4") ? json.get("4").getAsString() : ""; - - html.append("Vedlegg ").append(index + 1).append(":
"); - text.append("Vedlegg ").append(index + 1).append(":\n"); - - html.append(desc).append(" (").append(price).append(")
"); - text.append(desc).append(" (").append(price).append(")\n"); - - // Parse File Field (ID 1) - Can be multiple files! - if (json.has("1")) { - JsonElement fileEl = json.get("1"); - if (fileEl.isJsonArray()) { - // It is a real JSON array - JsonArray arr = fileEl.getAsJsonArray(); - for (int i = 0; i < arr.size(); i++) { - String url = arr.get(i).getAsString().replace("\\/", "/"); - html.append("Åpne fil ").append(i+1).append("
"); - text.append(url).append("\n"); - } - } else { - // It is a string. Check if it's a JSON string array like "[\"http...\"]" - String rawString = fileEl.getAsString(); - if (rawString.startsWith("[") && rawString.endsWith("]")) { - try { - JSONArray arr = new JSONArray(rawString); - for (int i = 0; i < arr.length(); i++) { - String url = arr.getString(i).replace("\\/", "/"); - html.append("Åpne fil ").append(i+1).append("
"); - text.append(url).append("\n"); - } - } catch (JSONException ex) { - // Fallback: simple cleanup - String clean = extractUrl(rawString); - html.append("Åpne fil
"); - text.append(clean).append("\n"); - } - } else { - // Just a plain URL string - String clean = extractUrl(rawString); - if(clean.startsWith("http")) { - html.append("Åpne fil
"); - text.append(clean).append("\n"); - } - } - } - } - html.append("
"); - text.append("\n"); - - } catch (Exception e) { - Log.e(TAG, "Error parsing child entry", e); - } - } else { - Log.e(TAG, "Failed to fetch child entry: " + response.code()); - } - // Gå til neste (uansett om denne feilet eller ei) - fetchChildEntriesRecursive(ids, index + 1, html, text, loader); - } - - @Override - public void onFailure(retrofit2.Call call, Throwable t) { - Log.e(TAG, "Network error fetching child entry", t); - // Hopp over ved feil - fetchChildEntriesRecursive(ids, index + 1, html, text, loader); - } - }); - } - - private void showFinalDialog(StringBuilder htmlBuilder, StringBuilder textBuilder) { - ScrollView scroll = new ScrollView(getContext()); - TextView text = new TextView(getContext()); - text.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); - text.setText(Html.fromHtml(htmlBuilder.toString(), Html.FROM_HTML_MODE_COMPACT)); - text.setPadding(40, 40, 40, 40); - scroll.addView(text); - - new AlertDialog.Builder(getContext()) - .setTitle("Detaljer") - .setView(scroll) - .setPositiveButton("Lukk", null) - .setNeutralButton("Del", (d, w) -> shareEntryDetails(textBuilder.toString())) - .create() - .show(); - } - - // Hjelpemetode for å rydde opp i URLer fra JSON (f.eks ["http://..."] -> http://...) - private String extractUrl(String rawValue) { - if (rawValue == null) return ""; - String clean = rawValue.replace("[", "") - .replace("]", "") - .replace("\"", "") - .replace("\\/", "/"); - return clean.trim(); - } - - private void shareEntryDetails(String text) { - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, text); - sendIntent.setType("text/plain"); - - Intent shareIntent = Intent.createChooser(sendIntent, "Del innsending via..."); - startActivity(shareIntent); - } - - private void prefillFormFromHistory(JSONObject latestEntry) { - if (latestEntry == null) return; - for (Map.Entry entry : inputViews.entrySet()) { - String fieldId = entry.getKey(); - View view = entry.getValue(); - - if (latestEntry.has(fieldId)) { - String value = latestEntry.optString(fieldId); - if (value == null || value.isEmpty()) continue; - - if (view instanceof EditText) { - ((EditText) view).setText(value); - } else if (view instanceof RadioGroup) { - RadioGroup group = (RadioGroup) view; - for (int i = 0; i < group.getChildCount(); i++) { - View child = group.getChildAt(i); - if (child instanceof RadioButton) { - Object tag = child.getTag(); - if (tag != null && tag.toString().equalsIgnoreCase(value)) { - ((RadioButton) child).setChecked(true); - break; - } - } - } - } else if (view instanceof CheckBox) { - if ("1".equals(value) || "true".equalsIgnoreCase(value) || ((CheckBox)view).getText().toString().equals(value)) { - ((CheckBox) view).setChecked(true); - } - } - } - } - updateStatus("Skjemaet er forhåndsutfylt fra din siste innsending."); - evaluateAllConditionalLogic(); - } - - private void clearInputs() { - for (View view : inputViews.values()) { - if (view instanceof EditText) { - ((EditText) view).setText(""); - } else if (view instanceof CheckBox) { - ((CheckBox) view).setChecked(false); - } else if (view instanceof RadioGroup) { - ((RadioGroup) view).clearCheck(); - } else if (view instanceof Button) { - Object tag = view.getTag(); - if (tag instanceof TextView) { - ((TextView) tag).setText("Ingen fil valgt"); - } - } - } - fileUploads.clear(); - nestedEntries.clear(); - if (nestedEntriesContainer != null) nestedEntriesContainer.removeAllViews(); - if (totalAmountView != null) totalAmountView.setText("Kr 0,00"); - } - - private static class NestedEntry { - String id; - String description; - String price; - NestedEntry(String id, String d, String p) { this.id = id; this.description = d; this.price = p; - } - } - - private void updateStatus(String msg) { - if (getActivity() != null && txtStatus != null) { - getActivity().runOnUiThread(() -> txtStatus.setText(msg)); - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsListFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.graphics.Color; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; - -public class FormsListFragment extends Fragment { - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_forms_list, container, false); - LinearLayout formsContainer = view.findViewById(R.id.forms_container); - - // Legger til knappene for de ulike skjemaene - addFormButton(formsContainer, "1. Ansatteopplysninger", 1); - addFormButton(formsContainer, "4. RUH (Rapport om uønsket hendelse)", 4); - addFormButton(formsContainer, "9. Sikkerhetskurs / Kompetansebevis", 9); - addFormButton(formsContainer, "10. HMS-bekreftelse", 10); - addFormButton(formsContainer, "11. Egenmelding", 11); - addFormButton(formsContainer, "12. Sjekkliste for firmabil", 12); - addFormButton(formsContainer, "14. SJA (Sikker Jobbanalyse)", 14); - addFormButton(formsContainer, "15. Fraværsvarsel", 15); - addFormButton(formsContainer, "16. Refusjon utlegg", 16); - addFormButton(formsContainer, "21. Forberedelse til medarbeidersamtale", 21); - addFormButton(formsContainer, "22. Medarbeiderundersøkelse", 22); - - return view; - } - - private void addFormButton(LinearLayout container, String title, int formId) { - Button btn = new Button(getContext()); - btn.setText(title); - btn.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå - btn.setTextColor(Color.WHITE); - btn.setPadding(30, 30, 30, 30); - - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(0, 0, 0, 20); // Litt avstand mellom knappene - btn.setLayoutParams(params); - - btn.setOnClickListener(v -> { - Bundle bundle = new Bundle(); - bundle.putInt("formId", formId); - // HER VAR FEILEN: Endret R.id.nav_forms til riktig action ID - Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle); - }); - - container.addView(btn); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\FormSubmission.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; -import java.util.Map; - -public class FormSubmission { - // Gravity Forms krever at dataene ligger inni "input_values" - @SerializedName("input_values") - public Map inputValues; - - public FormSubmission(Map inputValues) { - this.inputValues = inputValues; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; -import java.util.List; -import java.util.Map; - -public class GravityEntryResponse { - @SerializedName("total_count") - public int totalCount; - - @SerializedName("entries") - public List> entries; - // Vi bruker Map fordi Gravity Forms returnerer alle feltverdier som nøkkel/verdi par i roten av objektet. - // F.eks: { "id": "100", "form_id": "1", "1.3": "Ola", "1.6": "Nordmann" } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityField.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.annotations.SerializedName; -import java.util.List; - -public class GravityField { - @SerializedName("id") - public String id; - - @SerializedName("type") - public String type; - - @SerializedName("inputType") - public String inputType; - - @SerializedName("label") - public String label; - - @SerializedName("adminLabel") - public String adminLabel; - - @SerializedName("description") - public String description; - - @SerializedName("defaultValue") - public String defaultValue; - - @SerializedName("isRequired") - public boolean isRequired; - - @SerializedName("checkboxLabel") - public String checkboxLabel; - - @SerializedName("visibility") - public String visibility; - - @JsonAdapter(ChoicesAdapter.class) - @SerializedName("choices") - public List choices; - - @SerializedName("content") - public String content; - - @SerializedName("inputs") - public List inputs; - - @SerializedName("isHidden") - public boolean isHidden; - - // NYTT: For å sjekke om feltet er Read Only (f.eks dato i refusjon) - @SerializedName("gwreadonly_enable") - public boolean readOnly; - - @JsonAdapter(ConditionalLogicAdapter.class) - @SerializedName("conditionalLogic") - public ConditionalLogic conditionalLogic; - - @SerializedName("gppa-values-templates") - public java.util.Map gppaTemplates; - - @SerializedName("gpnfForm") - public String gpnfForm; - - public static class Choice { - @SerializedName("text") - public String text; - - @SerializedName("value") - public String value; - } - - public static class ConditionalLogic { - @SerializedName("actionType") - public String actionType; - - @SerializedName("logicType") - public String logicType; - - @SerializedName("rules") - public List rules; - } - - public static class Rule { - @SerializedName("fieldId") - public String fieldId; - - @SerializedName("operator") - public String operator; - - @SerializedName("value") - public String value; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityForm.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; -import java.util.List; - -public class GravityForm { - @SerializedName("id") - public int id; - - @SerializedName("title") - public String title; - - @SerializedName("description") - public String description; - - @SerializedName("is_active") - public String isActive; // "1" = Aktiv, "0" = Inaktiv - - @SerializedName("fields") - public List fields; -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookFragment.java -============================================================ -package com.kbs.kbsintranett; - -import androidx.fragment.app.Fragment; // Viktig import! - -public class HandbookFragment extends Fragment { - // Tomt innhold er OK, men klassen må hete det samme som filnavnet - // og den må arve fra (extends) Fragment. - - public HandbookFragment() { - super(R.layout.fragment_home); // Kobler til layouten automatisk - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class HomeFragment extends Fragment { - - private ActivityResultLauncher requestPermissionLauncher; - private RecyclerView calendarRecycler; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Håndter svar på kalendertillatelse - requestPermissionLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), - isGranted -> { - // Prøv å laste kalender på nytt (nå potensielt med personlig kalender) - if (calendarRecycler != null) { - fetchCalendarEvents(calendarRecycler); - } - } - ); - - // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) - startNotificationWorker(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_home, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // 0. Profil-knapp - View profileBtn = view.findViewById(R.id.btn_profile); - if (profileBtn != null) { - profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); - } - - // 1. Kalender oppsett - calendarRecycler = view.findViewById(R.id.recycler_calendar); - calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); - // Sett tom adapter midlertidig - calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); - - // "Se alle" knapp for kalender - TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); - if (viewAllCalendar != null) { - viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull)); - } - - // Sjekk tillatelse for personlig kalender - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { - fetchCalendarEvents(calendarRecycler); - } else { - // Spør om lov til å lese kalender - requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); - } - - // Spør også om lov til å sende varsler (Android 13+) - if (android.os.Build.VERSION.SDK_INT >= 33) { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS); - } - } - - // 2. Nyheter oppsett - RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); - newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); - newsRecycler.setNestedScrollingEnabled(false); - // Sett tom adapter midlertidig - newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); - - // "Se alle" knapp for nyheter - TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); - if (viewAllNews != null) { - viewAllNews.setOnClickListener(v -> { - Navigation.findNavController(view).navigate(R.id.action_home_to_newsFull); - }); - } - - fetchNewsFromWordpress(newsRecycler); - } - - private void fetchCalendarEvents(RecyclerView recyclerView) { - // 1. Hent personlige hendelser først (fra CalendarManager) - List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - - // 2. Hent API-hendelser fra WordPress - WordPressApiService apiService = RetrofitClient.getApiService(); - apiService.getCalendarEvents().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (!isAdded()) return; - - List apiEvents = new ArrayList<>(); - if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); // Formatér datoer - apiEvents.add(e); - } - } - - // 3. Flett listene (API + Personlig) og sorter - List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - - // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) - // (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende") - List upcomingEvents = new ArrayList<>(); - String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - - for (CalendarEvent e : merged) { - if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { - upcomingEvents.add(e); - } - } - - // 5. Vis kun de 5 første av de kommende - List top5 = new ArrayList<>(); - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (!isAdded()) return; - // Hvis API feiler, vis bare personlige events hvis vi har noen - if (!deviceEvents.isEmpty()) { - List top5 = new ArrayList<>(); - // Filtrer og plukk topp 5 fra lokale også - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); - } else { - List errorList = new ArrayList<>(); - errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS")); - recyclerView.setAdapter(new CalendarAdapter(errorList, null)); - } - } - }); - } - - private void fetchNewsFromWordpress(RecyclerView recyclerView) { - WordPressApiService apiService = RetrofitClient.getApiService(); - // Bruker getPosts som henter 5-10 innlegg med ?_embed - apiService.getPosts().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (getContext() == null) return; - - if (response.isSuccessful() && response.body() != null) { - List wpPosts = response.body(); - - // Datoformatering - SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); - targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - for (WpPost post : wpPosts) { - try { - Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); // Setter pen dato - } catch (Exception e) {} - } - - // Sett adapter med Click Listener - NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { - // Naviger til detaljvisning og send med post-objektet - Bundle bundle = new Bundle(); - bundle.putSerializable("post_data", post); // WpPost er nå Serializable - Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); - }); - recyclerView.setAdapter(adapter); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (getContext() == null) return; - // Ved feil, sett tom liste (eller vis feilmelding) - recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); - } - }); - } - - private void startNotificationWorker() { - // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter i HMS-kalenderen - PeriodicWorkRequest notifRequest = - new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) - .build(); - WorkManager.getInstance(requireContext()).enqueue(notifRequest); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.NavController; -import androidx.navigation.Navigation; - -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; -import com.google.android.gms.common.SignInButton; -import com.google.android.gms.common.api.ApiException; -import com.google.android.gms.tasks.Task; - -public class LoginFragment extends Fragment { - - private static final String TAG = "LoginFragment"; - private GoogleSignInClient mGoogleSignInClient; - private TextView statusText; - private SignInButton signInButton; - - // Håndterer resultatet fra Google-vinduet - private final ActivityResultLauncher signInLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - Task task = GoogleSignIn.getSignedInAccountFromIntent(result.getData()); - handleGoogleResult(task); - } - ); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_login, container, false); - statusText = view.findViewById(R.id.status_text); - signInButton = view.findViewById(R.id.sign_in_button); - signInButton.setSize(SignInButton.SIZE_WIDE); - - // Hent ID fra MainActivity - String clientId = MainActivity.GOOGLE_WEB_CLIENT_ID; - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(clientId) - .requestEmail() - .build(); - mGoogleSignInClient = GoogleSignIn.getClient(requireActivity(), gso); - - signInButton.setOnClickListener(v -> { - statusText.setText("Starter Google innlogging..."); - Intent signInIntent = mGoogleSignInClient.getSignInIntent(); - signInLauncher.launch(signInIntent); - }); - return view; - } - - private void handleGoogleResult(Task completedTask) { - try { - GoogleSignInAccount account = completedTask.getResult(ApiException.class); - // 1. Google er OK. Nå logger vi inn på WordPress. - statusText.setText("Google OK. Kobler til KBS Intranett..."); - signInButton.setEnabled(false); // Hindre dobbeltklikk - - String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; - - AuthRepository.loginToWordPress( - account.getIdToken(), - account.getDisplayName(), - account.getEmail(), - photoUrl, - new AuthRepository.AuthCallback() { - @Override - public void onSuccess(String role) { - // 2. Alt er OK! Naviger til Hjem. - if (isAdded()) { - statusText.setText("Innlogging OK!"); - NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); - navController.navigate(R.id.action_login_to_home); - } - } - - @Override - public void onError(String message) { - if (isAdded()) { - statusText.setText(message); - signInButton.setEnabled(true); - } - } - } - ); - } catch (ApiException e) { - // --- KORRIGERT FEILMELDING --- - Log.w(TAG, "signInResult:failed code=" + e.getStatusCode()); - String message; - if (e.getStatusCode() == 12500) { - message = "Konto ikke funnet, eller konto uten rettigheter."; - } else { - message = "Google-feil: " + e.getStatusCode(); - } - statusText.setText(message); - // --- SLUTT PÅ KORRIGERING --- - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginRequest.java -============================================================ -package com.kbs.kbsintranett; - -public class LoginRequest { - public String token; - - public LoginRequest(String token) { - this.token = token; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java -============================================================ -// FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; - -public class LoginResponse { - public boolean success; - - @SerializedName("full_cookie") - public String fullCookie; - - public String role; - - @SerializedName("user_id") - public int userId; - - // --- NYE FELTER --- - @SerializedName("first_name") - public String firstName; - - @SerializedName("last_name") - public String lastName; - - @SerializedName("stilling") // Sjekk at JSON-nøkkelen fra WP matcher dette - public String stilling; - - @SerializedName("mobiltelefon") // Sjekk at JSON-nøkkelen fra WP matcher dette - public String mobiltelefon; - // ------------------ - - public String message; -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java -============================================================ -package com.kbs.kbsintranett; - -import android.os.Bundle; -import android.util.Log; -import android.view.View; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.navigation.NavController; -import androidx.navigation.fragment.NavHostFragment; -import androidx.navigation.ui.NavigationUI; - -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; -import com.google.android.material.bottomnavigation.BottomNavigationView; - -public class MainActivity extends AppCompatActivity { - - // VIKTIG: Erstatt denne med din Web Client ID - public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; - private static final String TAG = "MainActivity"; - private NavController navController; - private BottomNavigationView bottomNav; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - // 1. Setup UI - bottomNav = findViewById(R.id.bottom_nav_view); - NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() - .findFragmentById(R.id.nav_host_fragment); - if (navHostFragment != null) { - navController = navHostFragment.getNavController(); - NavigationUI.setupWithNavController(bottomNav, navController); - - // Skjul meny på login-skjerm - navController.addOnDestinationChangedListener((controller, destination, arguments) -> { - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet - if (destination.getId() == R.id.navigation_login) { - bottomNav.setVisibility(View.GONE); - } else { - bottomNav.setVisibility(View.VISIBLE); - } - }); - } - - // 2. Start Silent Sign-In ved oppstart - checkLoginState(); - } - - private void checkLoginState() { - GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); - if (account == null) { - navigateToLogin(); - } else { - refreshGoogleToken(); - } - } - - private void refreshGoogleToken() { - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(GOOGLE_WEB_CLIENT_ID) - .requestEmail() - .build(); - GoogleSignInClient client = GoogleSignIn.getClient(this, gso); - - client.silentSignIn() - .addOnSuccessListener(account -> { - String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; - - AuthRepository.loginToWordPress( - account.getIdToken(), - account.getDisplayName(), - account.getEmail(), - photoUrl, - new AuthRepository.AuthCallback() { - @Override - public void onSuccess(String role) { - Log.d(TAG, "Silent login fullført. Rolle: " + role); - // Gå videre til Home hvis vi står på Login - if (navController != null && navController.getCurrentDestination() != null && - navController.getCurrentDestination().getId() == R.id.navigation_login) { - // Denne aksjonen finnes i mobile_navigation.xml - navController.navigate(R.id.action_login_to_home); - } - } - - @Override - public void onError(String message) { - Log.e(TAG, "Silent login feilet mot WP: " + message); - navigateToLogin(); - } - } - ); - }) - .addOnFailureListener(e -> { - Log.e(TAG, "Silent Sign-In feilet mot Google", e); - navigateToLogin(); - }); - } - - private void navigateToLogin() { - if (navController != null) { - if (navController.getCurrentDestination() != null && - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet - navController.getCurrentDestination().getId() != R.id.navigation_login) { - // Denne ID'en finnes i mobile_navigation.xml - navController.navigate(R.id.navigation_login); - } - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsAdapter.java -============================================================ -package com.kbs.kbsintranett; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; -import java.util.List; - -public class NewsAdapter extends RecyclerView.Adapter { - - private List posts; - private OnItemClickListener listener; // NYTT - - // Interface for klikk - public interface OnItemClickListener { - void onItemClick(WpPost post); - } - - // Oppdatert konstruktør - public NewsAdapter(List posts, OnItemClickListener listener) { - this.posts = posts; - this.listener = listener; - } - - // Overload for bakoverkompatibilitet (hvis du ikke sender listener) - public NewsAdapter(List posts) { - this.posts = posts; - this.listener = null; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - WpPost post = posts.get(position); - - holder.title.setText(post.getTitleStr()); - holder.excerpt.setText(post.getExcerptStr()); - holder.date.setText(post.date); - - String cat = post.getCategoryName(); - if (!cat.isEmpty()) { - holder.category.setText(cat); - holder.category.setVisibility(View.VISIBLE); - } else { - holder.category.setVisibility(View.GONE); - } - - String imageUrl = post.getFeaturedImageUrl(); - if (imageUrl != null && !imageUrl.isEmpty()) { - holder.image.setVisibility(View.VISIBLE); - Glide.with(holder.itemView.getContext()) - .load(imageUrl) - .transition(DrawableTransitionOptions.withCrossFade()) - .centerCrop() - .into(holder.image); - } else { - holder.image.setVisibility(View.GONE); - } - - // NYTT: Håndter klikk - holder.itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onItemClick(post); - } - }); - } - - @Override - public int getItemCount() { - return posts.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - TextView title, excerpt, date, category; - ImageView image; - - public ViewHolder(View view) { - super(view); - title = view.findViewById(R.id.news_title); - excerpt = view.findViewById(R.id.news_excerpt); - date = view.findViewById(R.id.news_date); - category = view.findViewById(R.id.news_category); - image = view.findViewById(R.id.news_image); - } - } - - // NYTT: Metode for å oppdatere listen etter filtrering - public void updateList(List newPosts) { - this.posts = newPosts; - notifyDataSetChanged(); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsDetailFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import com.bumptech.glide.Glide; - -public class NewsDetailFragment extends Fragment { - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_news_detail, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Hent data fra argumentene (sendt fra HomeFragment/NewsFullFragment) - if (getArguments() != null) { - WpPost post = (WpPost) getArguments().getSerializable("post_data"); - if (post != null) { - setupViews(view, post); - } - } - } - - private void setupViews(View view, WpPost post) { - Toolbar toolbar = view.findViewById(R.id.detail_toolbar); - toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).navigateUp()); - - ImageView image = view.findViewById(R.id.detail_image); - TextView title = view.findViewById(R.id.detail_title); - TextView category = view.findViewById(R.id.detail_category); - TextView date = view.findViewById(R.id.detail_date); - TextView author = view.findViewById(R.id.detail_author); // NY - TextView content = view.findViewById(R.id.detail_content); - - String imgUrl = post.getFeaturedImageUrl(); - if (imgUrl != null) { - Glide.with(this).load(imgUrl).centerCrop().into(image); - } else { - image.setBackgroundColor(getResources().getColor(android.R.color.darker_gray)); - } - - title.setText(post.getTitleStr()); - category.setText(post.getCategoryName()); - date.setText("Publisert: " + post.date); - - // NYTT: Sett forfatter - author.setText("Av: " + post.getAuthorName()); - - content.setText(Html.fromHtml(post.getContentStr(), Html.FROM_HTML_MODE_COMPACT)); - content.setMovementMethod(LinkMovementMethod.getInstance()); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsFullFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.Navigation; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class NewsFullFragment extends Fragment { - - private RecyclerView recyclerViewNews; - private RecyclerView recyclerViewCategories; - private ProgressBar progressBar; - private NewsAdapter newsAdapter; - private List allPosts = new ArrayList<>(); // Holder på ALLE postene - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_news_full, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - recyclerViewNews = view.findViewById(R.id.recycler_news_full); - recyclerViewCategories = view.findViewById(R.id.recycler_categories); - progressBar = view.findViewById(R.id.loading_news_full); - ImageView backBtn = view.findViewById(R.id.btn_back_news); - - // Setup Nyhetsliste - recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext())); - - // Setup Kategorier (Horisontal) - recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); - setupCategories(); - - backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); - - fetchAllNews(); - } - - private void setupCategories() { - // Listen over kategorier du ønsket - List categories = Arrays.asList( - "Alle", // Standard vis alt - "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", - "Ferieavvikling", "Fest og moro", "Generell drift", - "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" - ); - - CategoryAdapter catAdapter = new CategoryAdapter(categories, selectedCategory -> { - filterNews(selectedCategory); - }); - recyclerViewCategories.setAdapter(catAdapter); - } - - private void fetchAllNews() { - progressBar.setVisibility(View.VISIBLE); - // Hent 50 siste (bør holde for en "Siste nytt" liste, ellers må vi paginere) - RetrofitClient.getApiService().getAllPosts().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (!isAdded()) return; - progressBar.setVisibility(View.GONE); - - if (response.isSuccessful() && response.body() != null) { - allPosts = response.body(); - formatDates(allPosts); - - // Vis alle i starten - newsAdapter = new NewsAdapter(new ArrayList<>(allPosts), post -> { - Bundle bundle = new Bundle(); - bundle.putSerializable("post_data", post); - Navigation.findNavController(getView()).navigate(R.id.action_newsFull_to_newsDetail, bundle); - }); - recyclerViewNews.setAdapter(newsAdapter); - } else { - Toast.makeText(getContext(), "Klarte ikke laste nyheter", Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (!isAdded()) return; - progressBar.setVisibility(View.GONE); - Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); - } - }); - } - - private void filterNews(String category) { - if (newsAdapter == null) return; - - List filteredList = new ArrayList<>(); - - if (category.equals("Alle")) { - filteredList.addAll(allPosts); - } else { - for (WpPost post : allPosts) { - // Vi sjekker om kategorinavnet matcher - if (post.getCategoryName().equals(category)) { - filteredList.add(post); - } - } - } - - // Oppdater adapteren med den filtrerte listen - newsAdapter.updateList(filteredList); - } - - private void formatDates(List posts) { - SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); - targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - for (WpPost post : posts) { - try { - Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); - } catch (Exception e) {} - } - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsItem.java -============================================================ -package com.kbs.kbsintranett; - -public class NewsItem { - private String title; - private String excerpt; // Kort tekst/ingress - private String author; - - public NewsItem(String title, String excerpt, String author) { - this.title = title; - this.excerpt = excerpt; - this.author = author; - } - - public String getTitle() { return title; } - public String getExcerpt() { return excerpt; } - public String getAuthor() { return author; } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationWorker.java -============================================================ -package com.kbs.kbsintranett; - -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.work.Worker; -import androidx.work.WorkerParameters; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import retrofit2.Response; - -public class NotificationWorker extends Worker { - - private static final String CHANNEL_ID = "kbs_calendar_channel"; - private static final String PREFS_NAME = "KBSNotificationPrefs"; - - public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - // Dette kjører i bakgrunnen - try { - // Hent events synkront (ikke enqueue) - Response> response = RetrofitClient.getApiService().getCalendarEvents().execute(); - - if (response.isSuccessful() && response.body() != null) { - checkAndNotify(response.body()); - return Result.success(); - } else { - return Result.retry(); - } - } catch (IOException e) { - return Result.retry(); - } - } - - private void checkAndNotify(List events) { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - long now = System.currentTimeMillis(); - long fifteenMinutes = 15 * 60 * 1000; - - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - - for (CalendarEvent event : events) { - try { - Date eventDate; - if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); - else eventDate = sqlFormat.parse(event.getRawDate()); - - if (eventDate == null) continue; - - long diff = eventDate.getTime() - now; - - // Hvis eventet starter innen de neste 30 min, og ikke allerede varslet - if (diff > 0 && diff < (30 * 60 * 1000)) { - String eventId = event.getTitle() + event.getRawDate(); // Enkel ID - boolean alreadyNotified = prefs.getBoolean(eventId, false); - - if (!alreadyNotified) { - sendNotification(event.getTitle(), "Starter kl " + event.getTime()); - // Lagre at vi har varslet - prefs.edit().putBoolean(eventId, true).apply(); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private void sendNotification(String title, String content) { - NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); - manager.createNotificationChannel(channel); - } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her - .setContentTitle(title) - .setContentText(content) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setAutoCancel(true); - - manager.notify((int) System.currentTimeMillis(), builder.build()); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\ProfileFragment.java -============================================================ -package com.kbs.kbsintranett; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.NavController; -import androidx.navigation.Navigation; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; - -public class ProfileFragment extends Fragment { - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_profile, container, false); - // 1. Finn Views - ImageView closeBtn = view.findViewById(R.id.btn_close_profile); - ImageView profileImage = view.findViewById(R.id.profile_image); - TextView nameText = view.findViewById(R.id.profile_name); - TextView emailText = view.findViewById(R.id.profile_email); - TextView roleText = view.findViewById(R.id.profile_role); - Button logoutBtn = view.findViewById(R.id.btn_logout); - - // 2. Hent data fra UserManager - UserManager user = UserManager.getInstance(); - nameText.setText(user.getUserDisplayName()); - emailText.setText(user.getUserEmail()); - roleText.setText("Rolle: " + user.getUserRole()); - - // 3. Last bilde med Glide - if (user.getPhotoUrl() != null) { - Glide.with(this) - .load(user.getPhotoUrl()) - .apply(RequestOptions.circleCropTransform()) - .into(profileImage); - } - - // 4. Håndter "Lukk" (X) knapp - Gå tilbake til forrige skjerm - closeBtn.setOnClickListener(v -> { - Navigation.findNavController(view).navigateUp(); - }); - - // 5. Håndter utlogging - logoutBtn.setOnClickListener(v -> performLogout()); - - return view; - } - - private void performLogout() { - // A. Konfigurer Google Client - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(MainActivity.GOOGLE_WEB_CLIENT_ID) - .requestEmail() - .build(); - GoogleSignInClient client = GoogleSignIn.getClient(requireActivity(), gso); - - // B. Logg ut fra Google - client.signOut().addOnCompleteListener(task -> { - - // C. Tøm interne data - UserManager.getInstance().logout(); - RetrofitClient.clearClient(); - - // D. Naviger tilbake til Login-skjermen - NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); - // Denne aksjonen finnes i mobile_navigation.xml - navController.navigate(R.id.action_profile_to_login); - - Toast.makeText(getContext(), "Du er nå logget ut", Toast.LENGTH_SHORT).show(); - }); - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\RetrofitClient.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - -import java.io.IOException; -import java.util.List; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -public class RetrofitClient { - private static final String BASE_URL = "https://intranet.kbs.no/"; - private static Retrofit retrofit = null; - - public static WordPressApiService getApiService() { - if (retrofit == null) { - - // NYTT: Logging Interceptor - HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); - logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers) - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(logging) // Legg til loggingen her - .addInterceptor(new Interceptor() { - @Override - public Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - Request.Builder builder = originalRequest.newBuilder(); - - String dynamicCookie = UserManager.getInstance().getCookie(); - if (dynamicCookie != null && !dynamicCookie.isEmpty()) { - builder.header("Cookie", dynamicCookie); - } - - return chain.proceed(builder.build()); - } - }) - .build(); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter()) - .setLenient() // NYTT: Gjør parsing litt mer tilgivende - .create(); - - retrofit = new Retrofit.Builder() - .baseUrl(BASE_URL) - .client(client) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build(); - } - return retrofit.create(WordPressApiService.class); - } - - public static void clearClient() { - retrofit = null; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java -============================================================ -// FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java -package com.kbs.kbsintranett; - -import androidx.annotation.Nullable; - -/** - * UserManager fungerer som en global sesjon for appen. - * Den holder på informasjon om innlogget bruker, rettigheter og autentiserings-cookie. - */ -public class UserManager { - - private static UserManager instance; - - // Google Data - private String userDisplayName; - private String userEmail; - private String googleIdToken; - private String photoUrl; - - // WordPress Data - private int userId; - private String userRole; - private String currentCookie; - - // --- NYE FELTER --- - private String firstName; - private String lastName; - private String stilling; - private String mobiltelefon; - - private UserManager() { - // Initielt er ingen logget inn - } - - public static synchronized UserManager getInstance() { - if (instance == null) { - instance = new UserManager(); - } - return instance; - } - - /** - * Kalles når Google-innlogging er vellykket. - */ - public void setUserData(String name, String email, String token, @Nullable String photoUrl) { - this.userDisplayName = name; - this.userEmail = email; - this.googleIdToken = token; - this.photoUrl = photoUrl; - } - - // --- NY METODE FOR UTVIDET INFO --- - public void setExtendedUserInfo(String firstName, String lastName, String stilling, String mobiltelefon) { - this.firstName = firstName; - this.lastName = lastName; - this.stilling = stilling; - this.mobiltelefon = mobiltelefon; - } - - public void setCookie(String cookie) { - this.currentCookie = cookie; - } - - public void setUserRole(String role) { - this.userRole = role; - } - - public void setUserId(int id) { - this.userId = id; - } - - // ---------------- GETTERS ---------------- - - public String getUserDisplayName() { return userDisplayName != null ? userDisplayName : ""; } - public String getUserEmail() { return userEmail != null ? userEmail : ""; } - public String getGoogleIdToken() { return googleIdToken; } - public String getPhotoUrl() { return photoUrl; } - public String getCookie() { return currentCookie; } - public String getUserRole() { return userRole != null ? userRole : "subscriber"; } - public int getUserId() { return userId; } - - // --- NYE GETTERS --- - public String getFirstName() { return firstName != null ? firstName : ""; } - public String getLastName() { return lastName != null ? lastName : ""; } - public String getStilling() { return stilling != null ? stilling : ""; } - public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; } - - // ---------------- HJELPEMETODER ---------------- - - public boolean isLoggedIn() { - return userEmail != null && !userEmail.isEmpty(); - } - - public boolean isAdmin() { - return "administrator".equalsIgnoreCase(userRole); - } - - public boolean isEditorOrAbove() { - if (userRole == null) return false; - return userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor"); - } - - /** - * Nullstiller alt. Kalles ved utlogging. - */ - public void logout() { - userDisplayName = null; - userEmail = null; - googleIdToken = null; - photoUrl = null; - userRole = null; - currentCookie = null; - userId = 0; - - // Nullstill nye felter - firstName = null; - lastName = null; - stilling = null; - mobiltelefon = null; - } -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\WordPressApiService.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.JsonElement; -import java.util.List; -import java.util.Map; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Path; -import retrofit2.http.Multipart; -import retrofit2.http.Part; -import retrofit2.http.PartMap; -import retrofit2.http.Query; - -public interface WordPressApiService { - // 1. Hent nyheter - ENDRET: Lagt til &_embed for å få bilde og kategori - @GET("wp-json/wp/v2/posts?per_page=10&_embed") - Call> getPosts(); - - // 2. Hent et spesifikt skjema med ID - @GET("wp-json/gf/v2/forms/{id}") - Call getForm(@Path("id") int formId); - - // 3. SEND INN SKJEMA (JSON-data uten filer) - @POST("wp-json/gf/v2/forms/{id}/submissions") - Call submitForm(@Path("id") int formId, @Body FormSubmission submission); - - // 4. LOGIN MED GOOGLE - @POST("wp-json/kbs/v1/login") - Call googleLogin(@Body LoginRequest request); - - // 5. HENT LISTE AV SKJEMAER - @GET("wp-json/gf/v2/forms") - Call> getFormsListMap(); - - // 6. SEND INN SKJEMA (MULTIPART - for filopplasting) - @Multipart - @POST("wp-json/gf/v2/forms/{id}/submissions") - Call submitMultipartForm( - @Path("id") int formId, - @PartMap Map textFields, - @Part List files - ); - - // 7. HENT KALENDERHENDELSER - @GET("wp-json/kbs/v1/calendar/events") - Call> getCalendarEvents(); - - // 8. HENT INNSENDINGER (Entries) - LISTE - @GET("wp-json/gf/v2/entries") - Call getEntries( - @Query("form_ids") int formId, - @Query("search") String searchJson, - @Query("paging[page_size]") int pageSize - ); - - // 9. HENT ÉN ENKELT INNSENDING - @GET("wp-json/gf/v2/entries/{entry_id}") - Call getSingleEntry(@Path("entry_id") String entryId); - - // 10. HENT ALLE NYHETER (F.eks 50 stk) - Brukes av "Se alle" siden - @GET("wp-json/wp/v2/posts?per_page=50&_embed") - Call> getAllPosts(); -} - -============================================================ -FILSTI: app\src\main\java\com\kbs\kbsintranett\WpPost.java -============================================================ -package com.kbs.kbsintranett; - -import com.google.gson.annotations.SerializedName; -import java.io.Serializable; -import java.util.Arrays; -import java.util.List; - -public class WpPost implements Serializable { - @SerializedName("title") - public Rendered title; - - @SerializedName("excerpt") - public Rendered excerpt; - - @SerializedName("content") - public Rendered content; - - @SerializedName("date") - public String date; - - @SerializedName("_embedded") - public Embedded embedded; - - public static class Rendered implements Serializable { - @SerializedName("rendered") - public String renderedString; - } - - public static class Embedded implements Serializable { - @SerializedName("wp:featuredmedia") - public List mediaList; - - @SerializedName("wp:term") - public List> termList; - - // NYTT: Forfatter-liste - @SerializedName("author") - public List authorList; - } - - public static class Media implements Serializable { - @SerializedName("source_url") - public String sourceUrl; - } - - public static class Term implements Serializable { - @SerializedName("name") - public String name; - } - - // NYTT: Forfatter-klasse - public static class Author implements Serializable { - @SerializedName("name") - public String name; - } - - public String getTitleStr() { - return title != null ? title.renderedString : "Uten tittel"; - } - - public String getExcerptStr() { - return excerpt != null ? - android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString().trim() : ""; - } - - public String getContentStr() { - return content != null ? content.renderedString : ""; - } - - public String getFeaturedImageUrl() { - if (embedded != null && embedded.mediaList != null && !embedded.mediaList.isEmpty()) { - return embedded.mediaList.get(0).sourceUrl; - } - return null; - } - - // NYTT: Hent forfatternavn - public String getAuthorName() { - if (embedded != null && embedded.authorList != null && !embedded.authorList.isEmpty()) { - return embedded.authorList.get(0).name; - } - return "Ukjent"; // Fallback - } - - public String getCategoryName() { - if (embedded != null && embedded.termList != null && !embedded.termList.isEmpty()) { - List categories = embedded.termList.get(0); - if (categories == null || categories.isEmpty()) return ""; - - List priorityCategories = Arrays.asList( - "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", - "Ferieavvikling", "Fest og moro", "Generell drift", - "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" - ); - - for (Term term : categories) { - if (priorityCategories.contains(term.name)) return term.name; - } - for (Term term : categories) { - if (term.name.contains("Alle ansatte")) return "Til info"; - } - return categories.get(0).name; - } - return ""; - } -} - -============================================================ -FILSTI: app\src\main\res\drawable\bg_category_selected.xml -============================================================ - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\bg_category_unselected.xml -============================================================ - - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\ic_book.xml -============================================================ - - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\ic_form.xml -============================================================ - - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\ic_home.xml -============================================================ - - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\ic_launcher_background.xml -============================================================ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -============================================================ -FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml -============================================================ - - - - - - - - - - - - -============================================================ -FILSTI: app\src\main\res\layout\activity_main.xml -============================================================ - - - - - - - - - -============================================================ -FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml -============================================================ - - - - - - - - - - - -