From 56f14e9b9272475be262c22bf75ccc8441d6b3e4 Mon Sep 17 00:00:00 2001 From: ErolHaagenrud Date: Wed, 10 Dec 2025 10:03:07 +0100 Subject: [PATCH] =?UTF-8?q?F=C3=B8rste=20commit=20=E2=80=93=20initial=20ba?= =?UTF-8?q?ckup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/.name | 1 + .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 18 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/deviceManager.xml | 13 + .idea/gradle.xml | 19 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle.kts | 57 + app/proguard-rules.pro | 21 + .../kbsintranett/ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 44 + app/src/main/ic_launcher-playstore.png | Bin 0 -> 37349 bytes .../com/kbs/kbsintranett/AuthRepository.java | 72 + .../com/kbs/kbsintranett/CalendarAdapter.java | 51 + .../com/kbs/kbsintranett/CalendarEvent.java | 30 + .../com/kbs/kbsintranett/ChoicesAdapter.java | 31 + .../kbsintranett/ConditionalLogicAdapter.java | 26 + .../com/kbs/kbsintranett/FormSubmission.java | 14 + .../com/kbs/kbsintranett/FormsFragment.java | 541 +++ .../kbs/kbsintranett/FormsListFragment.java | 140 + .../kbsintranett/GravityEntryResponse.java | 15 + .../com/kbs/kbsintranett/GravityField.java | 82 + .../com/kbs/kbsintranett/GravityForm.java | 18 + .../kbs/kbsintranett/HandbookFragment.java | 12 + .../com/kbs/kbsintranett/HomeFragment.java | 202 ++ .../com/kbs/kbsintranett/LoginFragment.java | 114 + .../com/kbs/kbsintranett/LoginRequest.java | 9 + .../com/kbs/kbsintranett/LoginResponse.java | 32 + .../com/kbs/kbsintranett/MainActivity.java | 115 + .../com/kbs/kbsintranett/NewsAdapter.java | 48 + .../java/com/kbs/kbsintranett/NewsItem.java | 17 + .../com/kbs/kbsintranett/ProfileFragment.java | 85 + .../com/kbs/kbsintranett/RetrofitClient.java | 66 + .../com/kbs/kbsintranett/UserManager.java | 121 + .../kbs/kbsintranett/WordPressApiService.java | 60 + .../java/com/kbs/kbsintranett/WpPost.java | 31 + app/src/main/res/drawable/ic_book.xml | 5 + app/src/main/res/drawable/ic_form.xml | 5 + app/src/main/res/drawable/ic_home.xml | 5 + .../res/drawable/ic_launcher_background.xml | 74 + .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/layout/activity_main.xml | 33 + app/src/main/res/layout/fragment_forms.xml | 74 + app/src/main/res/layout/fragment_handbook.xml | 11 + app/src/main/res/layout/fragment_home.xml | 66 + app/src/main/res/layout/fragment_login.xml | 41 + app/src/main/res/layout/fragment_profile.xml | 87 + app/src/main/res/layout/item_calendar.xml | 69 + app/src/main/res/layout/item_news.xml | 44 + app/src/main/res/menu/bottom_nav_menu.xml | 17 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1584 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1886 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2944 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1052 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1198 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2038 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 2068 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 2678 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 4152 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 3138 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 5316 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6722 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 4718 bytes .../ic_launcher_foreground.webp | Bin 0 -> 8612 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 9510 bytes .../main/res/navigation/mobile_navigation.xml | 65 + app/src/main/res/values-night/themes.xml | 10 + app/src/main/res/values/colors.xml | 13 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 10 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../com/kbs/kbsintranett/ExampleUnitTest.java | 17 + build.gradle.kts | 4 + gradle.properties | 21 + gradle/libs.versions.toml | 22 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 251 ++ gradlew.bat | 94 + hele_prosjektet.txt | 3060 +++++++++++++++++ samle_kode.py | 62 + settings.gradle.kts | 24 + 93 files changed, 6485 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/kbs/kbsintranett/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/kbs/kbsintranett/AuthRepository.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/CalendarAdapter.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/ChoicesAdapter.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/ConditionalLogicAdapter.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/FormSubmission.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/FormsFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/GravityEntryResponse.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/GravityField.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/GravityForm.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/HandbookFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/HomeFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/LoginFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/LoginRequest.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/LoginResponse.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/MainActivity.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/NewsItem.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/ProfileFragment.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/UserManager.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java create mode 100644 app/src/main/java/com/kbs/kbsintranett/WpPost.java create mode 100644 app/src/main/res/drawable/ic_book.xml create mode 100644 app/src/main/res/drawable/ic_form.xml create mode 100644 app/src/main/res/drawable/ic_home.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/fragment_forms.xml create mode 100644 app/src/main/res/layout/fragment_handbook.xml create mode 100644 app/src/main/res/layout/fragment_home.xml create mode 100644 app/src/main/res/layout/fragment_login.xml create mode 100644 app/src/main/res/layout/fragment_profile.xml create mode 100644 app/src/main/res/layout/item_calendar.xml create mode 100644 app/src/main/res/layout/item_news.xml create mode 100644 app/src/main/res/menu/bottom_nav_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/navigation/mobile_navigation.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/kbs/kbsintranett/ExampleUnitTest.java create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 hele_prosjektet.txt create mode 100644 samle_kode.py create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..f675041 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +KBS Intranett \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..e762975 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..85220b6 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.kbs.kbsintranett" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.kbs.kbsintranett" + minSdk = 28 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + + // Nettverk og JSON-håndtering + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") + + // Navigation Component (KORRIGERT FOR KOTLIN DSL) + val navVersion = "2.8.5" // Oppdatert til en nyere, stabil versjon + implementation("androidx.navigation:navigation-fragment:$navVersion") + implementation("androidx.navigation:navigation-ui:$navVersion") + + implementation("com.google.android.gms:play-services-auth:20.7.0") + implementation("com.github.bumptech.glide:glide:4.16.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/kbs/kbsintranett/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/kbs/kbsintranett/ExampleInstrumentedTest.java new file mode 100644 index 0000000..b8d9f4e --- /dev/null +++ b/app/src/androidTest/java/com/kbs/kbsintranett/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.kbs.kbsintranett; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see 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()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4ed031f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..0809f19771cc13b2c9621123a1f74276483ec52e GIT binary patch literal 37349 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%0Ae}2HXMax;*t_NRvF|lrv<9j?-XSOw)Hzy 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 Pw?fZ3!0pSR0eAleO7llk literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java new file mode 100644 index 0000000..d9e5512 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java @@ -0,0 +1,72 @@ +// 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()); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarAdapter.java b/app/src/main/java/com/kbs/kbsintranett/CalendarAdapter.java new file mode 100644 index 0000000..2cbf099 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarAdapter.java @@ -0,0 +1,51 @@ +// 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 { // [cite: 31] + + private List events; + public CalendarAdapter(List events) { // [cite: 32] + this.events = events; + } // [cite: 33] + + @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); // [cite: 34] + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CalendarEvent event = events.get(position); + holder.day.setText(event.getDay()); // [cite: 35] + holder.month.setText(event.getMonth()); + // NYTT: Tidspunktet hentes nå fra getTime() som formateres i HomeFragment. + holder.time.setText(event.getTime()); + holder.title.setText(event.getTitle()); + } + + @Override + public int getItemCount() { + return events.size(); // [cite: 36] + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView day, month, title, time; // NYTT: Lagt til time + public ViewHolder(View view) { // [cite: 37] + super(view); + day = view.findViewById(R.id.cal_day); + month = view.findViewById(R.id.cal_month); // [cite: 38] + title = view.findViewById(R.id.cal_title); + time = view.findViewById(R.id.cal_time); // NYTT + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java new file mode 100644 index 0000000..983c4e3 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java @@ -0,0 +1,30 @@ +// FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java +package com.kbs.kbsintranett; + +public class CalendarEvent { + private String title; + private String rawDate; // NYTT: Holder den fulle, u-formaterte dato/tid-strengen fra API'et + private String day; // F.eks "12" + private String month; // F.eks "DES" + private String time; // NYTT: Brukes kun for visning av tid + + public CalendarEvent(String title, String time, String day, String month) { + this.title = title; + this.time = time; + this.day = day; + this.month = month; + } + + public CalendarEvent(String title, String rawDate) { + this.title = title; + this.rawDate = rawDate; + // La de andre feltene være null i starten, de fylles i HomeFragment + } + + public String getTitle() { return title; } + public String getTime() { return time; } + public String getDay() { return day; } + public String getMonth() { return month; } + + public String getRawDate() { return rawDate; } // NYTT +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/ChoicesAdapter.java b/app/src/main/java/com/kbs/kbsintranett/ChoicesAdapter.java new file mode 100644 index 0000000..0f0a7e1 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/ChoicesAdapter.java @@ -0,0 +1,31 @@ +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<>(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/ConditionalLogicAdapter.java b/app/src/main/java/com/kbs/kbsintranett/ConditionalLogicAdapter.java new file mode 100644 index 0000000..ef6eef7 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/ConditionalLogicAdapter.java @@ -0,0 +1,26 @@ +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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/FormSubmission.java b/app/src/main/java/com/kbs/kbsintranett/FormSubmission.java new file mode 100644 index 0000000..dc2b16a --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/FormSubmission.java @@ -0,0 +1,14 @@ +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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java b/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java new file mode 100644 index 0000000..7808bf0 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java @@ -0,0 +1,541 @@ +package com.kbs.kbsintranett; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Bundle; +import android.text.Html; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +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"; + + // ID 1 = Ansatteopplysninger (Skal ha autofyll fra historikk) + private static final int ID_ANSATTEOPPLYSNINGER = 1; + + private int formId = 1; + + private LinearLayout formContainer; + private LinearLayout historyContainer; + private TextView txtStatus; + private TextView lblHistory; // Overskriften "Tidligere innsendinger" + private ProgressBar loadingSpinner; + + private Map dynamicFields = new HashMap<>(); + private Map requiredFieldsMap = new HashMap<>(); + + private final OkHttpClient client = new OkHttpClient(); + + @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); + txtStatus = view.findViewById(R.id.txt_status); + lblHistory = view.findViewById(R.id.lbl_history); + loadingSpinner = view.findViewById(R.id.loading_spinner); + + // Fallback hvis XML feiler + if (formContainer == null) { + // Hvis brukeren ikke har oppdatert XML ennå, unngå krasj + formContainer = new LinearLayout(getContext()); + } + + if (getArguments() != null) { + int argId = getArguments().getInt("formId", 0); + if (argId != 0) formId = argId; + } + + fetchFormStructure(); + + return view; + } + + 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) { + GravityForm form = response.body(); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); + renderDynamicForm(form); + 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(); + dynamicFields.clear(); + requiredFieldsMap.clear(); + updateStatus(""); + + // Tittel + TextView title = new TextView(getContext()); + title.setText(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()); + formDesc.setText(form.description); + formDesc.setPadding(0, 0, 0, 40); + formContainer.addView(formDesc); + } + + if (form.fields == null) return; + + UserManager user = UserManager.getInstance(); + boolean isPersonaliaSection = true; + + boolean hasFilledName = false; + boolean hasFilledEmail = false; + boolean hasFilledPhone = false; + boolean hasFilledJob = false; + + for (GravityField field : form.fields) { + + if ("hidden".equals(field.type)) continue; + if (field.isHidden) continue; + if ("hidden".equals(field.visibility)) continue; + + if ("section".equals(field.type)) { + String labelLower = field.label.toLowerCase(); + isPersonaliaSection = labelLower.contains("personalia"); + addSectionHeader(field.label, field.description); + continue; + } + + if ("html".equals(field.type)) { + if (field.content != null && field.content.toLowerCase().contains("pårørende")) { + isPersonaliaSection = false; + } + if (field.content != null && !field.content.isEmpty()) { + TextView htmlView = new TextView(getContext()); + htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT)); + htmlView.setPadding(0, 40, 0, 10); + formContainer.addView(htmlView); + } + continue; + } + + if (field.inputs != null && !field.inputs.isEmpty()) { + renderCompositeField(field, isPersonaliaSection); + continue; + } + + // ENKELTFELT + TextView label = new TextView(getContext()); + String labelText = field.label; + if (field.isRequired) labelText += " *"; + label.setText(labelText); + label.setTextColor(Color.DKGRAY); + label.setPadding(0, 25, 0, 5); + formContainer.addView(label); + + EditText input = new EditText(getContext()); + input.setPadding(30, 30, 30, 30); + input.setBackgroundResource(android.R.drawable.edit_text); + + // Autofyll fra UserManager (Standardinfo som alltid er greit å fylle ut) + if (isPersonaliaSection) { + String lowerLabel = field.label.toLowerCase(); + + if (!hasFilledName && (lowerLabel.equals("navn") || lowerLabel.equals("navn *"))) { + input.setText(user.getUserDisplayName()); + hasFilledName = true; + } + else if (!hasFilledEmail && lowerLabel.contains("e-post") && lowerLabel.contains("arbeid")) { + input.setText(user.getUserEmail()); + hasFilledEmail = true; + } + else if (!hasFilledJob && lowerLabel.contains("stilling")) { + input.setText(user.getStilling()); + hasFilledJob = true; + } + else if (!hasFilledPhone && (lowerLabel.contains("mobil") || lowerLabel.equals("mobiltelefon"))) { + input.setText(user.getMobiltelefon()); + hasFilledPhone = true; + } + } + + 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); + } + + formContainer.addView(input); + + if (field.description != null && !field.description.isEmpty()) { + addDescription(field.description); + } + + dynamicFields.put(String.valueOf(field.id), input); + requiredFieldsMap.put(String.valueOf(field.id), field.isRequired); + } + + // SEND-KNAPP + 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); + } + + private void renderCompositeField(GravityField parentField, boolean isPersonaliaSection) { + TextView groupLabel = new TextView(getContext()); + String groupText = parentField.label; + if (parentField.isRequired) groupText += " *"; + groupLabel.setText(groupText); + groupLabel.setTextColor(Color.DKGRAY); + groupLabel.setTypeface(null, Typeface.BOLD); + groupLabel.setPadding(0, 30, 0, 5); + formContainer.addView(groupLabel); + + UserManager user = UserManager.getInstance(); + String lowerParentLabel = parentField.label.toLowerCase(); + + List inputs = new ArrayList<>(parentField.inputs); + + // Sortering for adresse + if (parentField.type.equals("address")) { + Collections.sort(inputs, new Comparator() { + @Override + public int compare(GravityField f1, GravityField f2) { + int s1 = getAddressScore(f1.label); + int s2 = getAddressScore(f2.label); + return Integer.compare(s1, s2); + } + }); + } + + for (GravityField subField : inputs) { + if (subField.isHidden) continue; + if ("hidden".equals(subField.visibility)) continue; + + TextView subLabel = new TextView(getContext()); + String subLabelText = subField.label; + + boolean isSubRequired = parentField.isRequired; + if (parentField.type.equals("address") && 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); + formContainer.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 (isPersonaliaSection && lowerParentLabel.contains("navn") && !lowerParentLabel.contains("pårørende")) { + String lowerSubLabel = subField.label.toLowerCase(); + if (lowerSubLabel.contains("fornavn")) { + subInput.setText(user.getFirstName()); + } + else if (lowerSubLabel.contains("etternavn")) { + subInput.setText(user.getLastName()); + } + } + + formContainer.addView(subInput); + + dynamicFields.put(subField.id, subInput); + requiredFieldsMap.put(subField.id, isSubRequired); + } + + if (parentField.description != null && !parentField.description.isEmpty()) { + addDescription(parentField.description); + } + } + + 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 addDescription(String text) { + TextView desc = new TextView(getContext()); + desc.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)); + desc.setTextSize(12); + desc.setTextColor(Color.GRAY); + desc.setPadding(5, 5, 5, 0); + formContainer.addView(desc); + } + + private void addSectionHeader(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, 50, 0, 5); + formContainer.addView(sectionHeader); + + if (descText != null && !descText.isEmpty()) { + addDescription(descText); + } + + View line = new View(getContext()); + line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2)); + line.setBackgroundColor(Color.LTGRAY); + formContainer.addView(line); + } + + 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 Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + Log.e(TAG, "Kunne ikke hente historikk", e); + } + + @Override + public void onResponse(@NonNull 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); + + // VIKTIG: AUTO-FYLL SKJER NÅ KUN FOR SKJEMA ID 1 + if (entries.length() > 0 && formId == ID_ANSATTEOPPLYSNINGER) { + try { + prefillFormFromHistory(entries.getJSONObject(0)); + } catch (JSONException e) { e.printStackTrace(); } + } + }); + } + } + } catch (JSONException e) { e.printStackTrace(); } + } + } + }); + } + + private void prefillFormFromHistory(JSONObject latestEntry) { + for (Map.Entry mapEntry : dynamicFields.entrySet()) { + String fieldId = mapEntry.getKey(); + EditText input = mapEntry.getValue(); + + if (input.getText().length() > 0) continue; + + if (latestEntry.has(fieldId)) { + String prevValue = latestEntry.optString(fieldId); + if (!prevValue.isEmpty()) { + input.setText(prevValue); + } + } + } + updateStatus("Skjemaet er forhåndsutfylt fra din forrige innsending."); + } + + private void submitDynamicForm() { + JSONObject inputValues = new JSONObject(); + boolean hasValues = false; + + for (Map.Entry entry : dynamicFields.entrySet()) { + String fieldId = entry.getKey(); + EditText input = entry.getValue(); + String value = input.getText().toString().trim(); + + Boolean isRequired = requiredFieldsMap.get(fieldId); + if (isRequired != null && isRequired && value.isEmpty()) { + input.setError("Må fylles ut"); + input.requestFocus(); + return; + } + + if (!value.isEmpty()) { + try { + inputValues.put("input_" + fieldId, value); + hasValues = true; + } catch (JSONException e) { e.printStackTrace(); } + } + } + + if (!hasValues) { + Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show(); + return; + } + + updateStatus("Sender inn..."); + MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + RequestBody body = RequestBody.create(JSON, inputValues.toString()); + String url = BASE_URL_GF + "/forms/" + formId + "/submissions"; + String cookie = UserManager.getInstance().getCookie(); + + if (cookie == null) { + updateStatus("Du er ikke logget inn."); + return; + } + + Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + updateStatus("Nettverksfeil: " + e.getMessage()); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + if (response.isSuccessful()) { + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + Toast.makeText(getContext(), "Skjema sendt!", Toast.LENGTH_LONG).show(); + fetchFormEntries(); + updateStatus("Innsending OK"); + }); + } + } else { + updateStatus("Feil fra server: " + response.code()); + } + } + }); + } + + private void showHistory(JSONArray entries) { + if (historyContainer == null) return; + historyContainer.removeAllViews(); + + if (entries.length() == 0) { + // Skjul overskriften hvis det ikke er historikk + if (lblHistory != null) lblHistory.setVisibility(View.GONE); + return; + } else { + if (lblHistory != null) lblHistory.setVisibility(View.VISIBLE); + } + + try { + for (int i = 0; i < entries.length(); i++) { + JSONObject entry = entries.getJSONObject(i); + String date = entry.optString("date_created"); + TextView item = new TextView(getContext()); + item.setText(date); + item.setPadding(10, 20, 10, 20); + + // Gjør historikken klikkbar (for fremtidig funksjonalitet) + item.setBackgroundResource(android.R.drawable.list_selector_background); + + 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(); } + } + + private void updateStatus(String msg) { + if (getActivity() != null && txtStatus != null) { + getActivity().runOnUiThread(() -> txtStatus.setText(msg)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java new file mode 100644 index 0000000..9521f21 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java @@ -0,0 +1,140 @@ +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 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 java.util.Map; +import java.util.TreeMap; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class FormsListFragment extends Fragment { + + private LinearLayout container; + private ProgressBar loadingSpinner; + private TextView txtStatus; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // Vi gjenbruker samme layout som detaljvisningen, siden den nå er ryddig + View view = inflater.inflate(R.layout.fragment_forms, container, false); + + this.container = view.findViewById(R.id.form_container); + this.loadingSpinner = view.findViewById(R.id.loading_spinner); + this.txtStatus = view.findViewById(R.id.txt_status); + + // Skjul "Tidligere innsendinger" tekst og container på denne siden, + // da vi kun skal vise en meny her. + View historyTitle = view.findViewById(R.id.historyContainer); // Egentlig containeren, men vi skjuler alt under + if (historyTitle != null) historyTitle.setVisibility(View.GONE); + + // Finn og skjul "Tidligere innsendinger" overskriften hvis mulig + // (Siden vi bruker en delt XML er dette litt "hacky", men funker) + LinearLayout mainLayout = view.findViewById(R.id.main_layout); + if (mainLayout != null && mainLayout.getChildCount() > 4) { + // Skjuler elementene nederst som hører til historikk + for(int i = 0; i < mainLayout.getChildCount(); i++) { + View child = mainLayout.getChildAt(i); + if (child instanceof TextView && ((TextView)child).getText().toString().contains("Tidligere")) { + child.setVisibility(View.GONE); + } + } + } + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + fetchFormsList(); + } + + private void fetchFormsList() { + if (container != null) container.removeAllViews(); + if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE); + if (txtStatus != null) txtStatus.setText(""); + + // Hent listen over skjemaer (Map ID -> Skjema) + RetrofitClient.getApiService().getFormsListMap().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (getContext() == null) return; + + // SKJUL SPINNER NÅR VI FÅR SVAR + if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); + + if (response.isSuccessful() && response.body() != null) { + Map formMap = response.body(); + + if (formMap.isEmpty()) { + if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet."); + return; + } + + // Vi sorterer listen basert på ID (eller tittel) for ryddighet + Map sortedMap = new TreeMap<>(formMap); + + for (Map.Entry entry : sortedMap.entrySet()) { + GravityForm form = entry.getValue(); + // Sjekk at skjemaet er aktivt og har en tittel + if (form != null && form.title != null) { + addFormButton(form); + } + } + } else { + if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")"); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (getContext() == null) return; + // SKJUL SPINNER VED FEIL + if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); + if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage()); + } + }); + } + + private void addFormButton(GravityForm form) { + Button button = new Button(getContext()); + button.setText(form.title.toUpperCase()); // Store bokstaver ser ofte ryddigere ut i menyer + button.setTextColor(Color.WHITE); + button.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå + button.setTextSize(14); + button.setPadding(30, 30, 30, 30); + + // Klikk-lytter + button.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putInt("formId", form.id); + Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle); + }); + + // Layout parametere (Marginer) + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, 0, 0, 20); // Avstand mellom knappene + button.setLayoutParams(params); + + container.addView(button); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityEntryResponse.java b/app/src/main/java/com/kbs/kbsintranett/GravityEntryResponse.java new file mode 100644 index 0000000..bfe2980 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/GravityEntryResponse.java @@ -0,0 +1,15 @@ +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" } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityField.java b/app/src/main/java/com/kbs/kbsintranett/GravityField.java new file mode 100644 index 0000000..a9f70d9 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/GravityField.java @@ -0,0 +1,82 @@ +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("label") + public String label; + + @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; + + @JsonAdapter(ConditionalLogicAdapter.class) + @SerializedName("conditionalLogic") + public ConditionalLogic conditionalLogic; + + // NYTT: For å lese Populate Anything-regler + @SerializedName("gppa-values-templates") + public java.util.Map gppaTemplates; + + public static class Choice { + @SerializedName("text") + public String text; + + @SerializedName("value") + public String value; + } + + public static class ConditionalLogic { + @SerializedName("actionType") + public String actionType; // "show" eller "hide" + + @SerializedName("logicType") + public String logicType; // "all" eller "any" + + @SerializedName("rules") + public List rules; + } + + public static class Rule { + @SerializedName("fieldId") + public String fieldId; + + @SerializedName("operator") + public String operator; // "is", "isnot", etc. + + @SerializedName("value") + public String value; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityForm.java b/app/src/main/java/com/kbs/kbsintranett/GravityForm.java new file mode 100644 index 0000000..cd54690 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/GravityForm.java @@ -0,0 +1,18 @@ +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("fields") + public List fields; // En liste med alle feltene i skjemaet +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HandbookFragment.java b/app/src/main/java/com/kbs/kbsintranett/HandbookFragment.java new file mode 100644 index 0000000..6cd7db3 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/HandbookFragment.java @@ -0,0 +1,12 @@ +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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java new file mode 100644 index 0000000..6f4eda9 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -0,0 +1,202 @@ +// FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java +package com.kbs.kbsintranett; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +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.ParseException; +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 retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class HomeFragment extends Fragment { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // Laster inn layouten fra XML (fragment_home.xml) + return inflater.inflate(R.layout.fragment_home, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // --------------------------------------------------------- + // 0. SETT OPP PROFIL-KNAPP (Ny!) + // --------------------------------------------------------- + // Vi finner ikonet vi la til i XML og sier at det skal gå til Profil-siden + View profileBtn = view.findViewById(R.id.btn_profile); + if (profileBtn != null) { + profileBtn.setOnClickListener(v -> { + Navigation.findNavController(view).navigate(R.id.navigation_profile); + }); + } + + // --------------------------------------------------------- + // 1. SETT OPP KALENDER (Henter fra WordPress) + // --------------------------------------------------------- + RecyclerView calendarRecycler = view.findViewById(R.id.recycler_calendar); + calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + + // Starter henting av kalenderdata + fetchCalendarEvents(calendarRecycler); + + // --------------------------------------------------------- + // 2. SETT OPP NYHETER (Hentes fra WordPress) + // --------------------------------------------------------- + RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); + newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + + // Gjør at scrollen flyter bedre inni NestedScrollView (hvis du bruker det i XML) + newsRecycler.setNestedScrollingEnabled(false); + // Start henting av ekte data + fetchNewsFromWordpress(newsRecycler); + } + + /** + * Henter kalenderhendelser fra WordPress via RetrofitClient + */ + private void fetchCalendarEvents(RecyclerView recyclerView) { + // 1. Hent API-tjenesten vår + WordPressApiService apiService = RetrofitClient.getApiService(); + // 2. Send forespørsel til nettet (Asynkront) + apiService.getCalendarEvents().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (getContext() == null || response.body() == null) return; + + List rawEvents = response.body(); + List formattedEvents = new ArrayList<>(); + + // Formater for parsing og visning + SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + SimpleDateFormat dayFormat = new SimpleDateFormat("dd", Locale.getDefault()); + dayFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + // Bruker norsk locale for måned (Jan, Feb, Mar, etc.) + SimpleDateFormat monthFormat = new SimpleDateFormat("MMM", new Locale("no", "NO")); + monthFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault()); + timeFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + for (CalendarEvent event : rawEvents) { + try { + // Bruker getRawDate() fra CalendarEvent.java (oppdatert) + Date date = apiFormat.parse(event.getRawDate()); + String day = dayFormat.format(date); + String month = monthFormat.format(date).toUpperCase(Locale.getDefault()); + String startTime = timeFormat.format(date); + + // Bruker den gamle konstruktøren for å sette formaterte data i adapteren + formattedEvents.add(new CalendarEvent( + event.getTitle(), + startTime, + day, + month + )); + } catch (ParseException e) { + e.printStackTrace(); + // Håndterer feil i parsing av dato/tid ved å vise rå data + formattedEvents.add(new CalendarEvent(event.getTitle(), "Ukjent", event.getDay(), event.getMonth())); + } + } + recyclerView.setAdapter(new CalendarAdapter(formattedEvents)); + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (getContext() == null) return; + System.err.println("Kalender Nettverksfeil: " + t.getMessage()); + // Vis feilmelding i RecyclerView + List errorList = new ArrayList<>(); + errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverket ditt.", "00", "FEIL")); + recyclerView.setAdapter(new CalendarAdapter(errorList)); + } + }); + } + + /** + * Henter nyheter fra WordPress via RetrofitClient + */ + private void fetchNewsFromWordpress(RecyclerView recyclerView) { + // 1. Hent API-tjenesten vår + WordPressApiService apiService = RetrofitClient.getApiService(); + // 2. Send forespørsel til nettet (Asynkront) + apiService.getPosts().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + // Sjekk om appen fortsatt lever (viktig for å unngå krasj) + if (getContext() == null) return; + + if (response.isSuccessful() && response.body() != null) { + // 3. Suksess! Vi fikk data fra WordPress. + List wpPosts = response.body(); + List newsList = new ArrayList<>(); + + // Datoformatering for nyhetene + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + + // Konverter fra "WpPost" (API-format) til "NewsItem" (App-format) + for (WpPost post : wpPosts) { + String formattedDate = post.date; + + try { + // API-datoen (post.date) er i formatet "yyyy-MM-dd'T'HH:mm:ss" + Date date = rawFormat.parse(post.date); + formattedDate = targetFormat.format(date); + } catch (ParseException e) { + System.err.println("Feil ved parsing av nyhetsdato: " + e.getMessage()); + } + + newsList.add(new NewsItem( + post.getTitleStr(), + post.getExcerptStr(), + "Publisert: " + formattedDate + )); + } + + // 4. Send listen til Adapteren slik at den vises på skjermen + NewsAdapter adapter = new NewsAdapter(newsList); + recyclerView.setAdapter(adapter); + } else { + System.err.println("Feil: Fikk svar, men noe var galt med dataene: " + response.code()); + // Her kunne vi vist en "Ingen nyheter"-tekst + // (Løsningen har allerede lagt inn fallback i onFailure) + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + // Nettverksfeil (Ingen nett, feil URL, etc) + if (getContext() == null) return; + System.err.println("Nettverksfeil: " + t.getMessage()); + // Legg til en "Feilmelding" i listen så brukeren ser det + List errorList = new ArrayList<>(); + errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System")); + recyclerView.setAdapter(new NewsAdapter(errorList)); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/LoginFragment.java b/app/src/main/java/com/kbs/kbsintranett/LoginFragment.java new file mode 100644 index 0000000..df665c5 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/LoginFragment.java @@ -0,0 +1,114 @@ +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 --- + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/LoginRequest.java b/app/src/main/java/com/kbs/kbsintranett/LoginRequest.java new file mode 100644 index 0000000..dfccb8b --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/LoginRequest.java @@ -0,0 +1,9 @@ +package com.kbs.kbsintranett; + +public class LoginRequest { + public String token; + + public LoginRequest(String token) { + this.token = token; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java b/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java new file mode 100644 index 0000000..cfbda46 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java @@ -0,0 +1,32 @@ +// 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; +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java new file mode 100644 index 0000000..cfd03b1 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java @@ -0,0 +1,115 @@ +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 = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.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); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java b/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java new file mode 100644 index 0000000..767ad75 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java @@ -0,0 +1,48 @@ +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 NewsAdapter extends RecyclerView.Adapter { + + private List newsList; + + public NewsAdapter(List newsList) { + this.newsList = newsList; + } + + @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) { + NewsItem item = newsList.get(position); + holder.title.setText(item.getTitle()); + holder.excerpt.setText(item.getExcerpt()); + holder.author.setText("Av: " + item.getAuthor()); + } + + @Override + public int getItemCount() { + return newsList.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView title, excerpt, author; + public ViewHolder(View view) { + super(view); + title = view.findViewById(R.id.news_title); + excerpt = view.findViewById(R.id.news_excerpt); + author = view.findViewById(R.id.news_author); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NewsItem.java b/app/src/main/java/com/kbs/kbsintranett/NewsItem.java new file mode 100644 index 0000000..f34d7bd --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/NewsItem.java @@ -0,0 +1,17 @@ +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; } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/ProfileFragment.java b/app/src/main/java/com/kbs/kbsintranett/ProfileFragment.java new file mode 100644 index 0000000..a43ca41 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/ProfileFragment.java @@ -0,0 +1,85 @@ +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(); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java new file mode 100644 index 0000000..c75de16 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java @@ -0,0 +1,66 @@ +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 retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class RetrofitClient { + private static final String BASE_URL = "https://intranet.kbs.no/"; + + // VI FJERNER FAKE_COOKIE HERFRA! Den trengs ikke lenger. + + private static Retrofit retrofit = null; + + public static WordPressApiService getApiService() { + // Vi må bygge klienten på nytt hvis vi logger ut/inn, men for enkelhets skyld + // sjekker vi bare null her. + if (retrofit == null) { + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + Request.Builder builder = originalRequest.newBuilder(); + + // 1. Hent cookie fra UserManager + String dynamicCookie = UserManager.getInstance().getCookie(); + + // 2. Hvis vi har en cookie, legg den til i headeren + 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()) + .create(); + + retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build(); + } + return retrofit.create(WordPressApiService.class); + } + + // Hjelpemetode for å nullstille Retrofit ved utlogging + public static void clearClient() { + retrofit = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/UserManager.java b/app/src/main/java/com/kbs/kbsintranett/UserManager.java new file mode 100644 index 0000000..903e0d3 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/UserManager.java @@ -0,0 +1,121 @@ +// 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java new file mode 100644 index 0000000..ca274b5 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java @@ -0,0 +1,60 @@ +// 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; // NYTT + +public interface WordPressApiService { + // 1. Hent nyheter + @GET("wp-json/wp/v2/posts?per_page=5") + 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) - NYTT + @GET("wp-json/gf/v2/entries") + Call getEntries( + @Query("form_ids") int formId, + @Query("search") String searchJson, + @Query("paging[page_size]") int pageSize + ); +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WpPost.java b/app/src/main/java/com/kbs/kbsintranett/WpPost.java new file mode 100644 index 0000000..d7504e6 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/WpPost.java @@ -0,0 +1,31 @@ +package com.kbs.kbsintranett; + +import com.google.gson.annotations.SerializedName; + +public class WpPost { + // WordPress sender tittelen som et objekt: "title": { "rendered": "Overskrift" } + @SerializedName("title") + public Rendered title; + + @SerializedName("excerpt") + public Rendered excerpt; + + @SerializedName("date") + public String date; + + // Hjelpeklasse for å hente ut teksten inni "rendered" + public static class Rendered { + @SerializedName("rendered") + public String renderedString; + } + + // En hjelpemetode for å få ren tekst ut (fjerner HTML-koder hvis nødvendig) + public String getTitleStr() { + return title != null ? title.renderedString : "Uten tittel"; + } + + public String getExcerptStr() { + // En enkel rensing av HTML-tags (f.eks

) + return excerpt != null ? android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString() : ""; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_book.xml b/app/src/main/res/drawable/ic_book.xml new file mode 100644 index 0000000..2f548a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_book.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_form.xml b/app/src/main/res/drawable/ic_form.xml new file mode 100644 index 0000000..4014ac5 --- /dev/null +++ b/app/src/main/res/drawable/ic_form.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..20cb4d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..42acff3 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_forms.xml b/app/src/main/res/layout/fragment_forms.xml new file mode 100644 index 0000000..cf5a7b2 --- /dev/null +++ b/app/src/main/res/layout/fragment_forms.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_handbook.xml b/app/src/main/res/layout/fragment_handbook.xml new file mode 100644 index 0000000..e6fda5a --- /dev/null +++ b/app/src/main/res/layout/fragment_handbook.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..76853e9 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..ed770d6 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..2787143 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + +