kbsintranett/hele_prosjektet.txt

3859 lines
153 KiB
Text
Raw Normal View History

2025-12-10 10:03:07 +01:00
Dette er kildekoden til et Android Studio-prosjekt.
Hver fil er separert med overskrifter.
============================================================
FILSTI: build.gradle.kts
============================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
============================================================
FILSTI: settings.gradle.kts
============================================================
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "KBS Intranett"
include(":app")
============================================================
FILSTI: app\build.gradle.kts
============================================================
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")
}
============================================================
FILSTI: app\proguard-rules.pro
============================================================
# 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
============================================================
FILSTI: app\src\androidTest\java\com\kbs\kbsintranett\ExampleInstrumentedTest.java
============================================================
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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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
============================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.kbs.kbsintranett">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KBSIntranett"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
============================================================
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<LoginResponse>() {
@Override
public void onResponse(Call<LoginResponse> call, Response<LoginResponse> 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<LoginResponse> 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
============================================================
// 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<CalendarAdapter.ViewHolder> { // [cite: 31]
private List<CalendarEvent> events;
public CalendarAdapter(List<CalendarEvent> 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
}
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java
============================================================
// 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
}
============================================================
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<List<GravityField.Choice>> {
@Override
public List<GravityField.Choice> 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<GravityField.Choice> 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<GravityField.ConditionalLogic> {
@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;
2025-12-10 14:00:27 +01:00
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.database.Cursor;
2025-12-10 10:03:07 +01:00
import android.graphics.Color;
import android.graphics.Typeface;
2025-12-10 14:00:27 +01:00
import android.net.Uri;
2025-12-10 10:03:07 +01:00
import android.os.Bundle;
2025-12-10 14:00:27 +01:00
import android.provider.OpenableColumns;
import android.text.Editable;
2025-12-10 10:03:07 +01:00
import android.text.Html;
import android.text.InputType;
2025-12-10 14:00:27 +01:00
import android.text.TextUtils;
import android.text.TextWatcher;
2025-12-10 10:03:07 +01:00
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
2025-12-10 14:00:27 +01:00
import android.widget.ArrayAdapter;
2025-12-10 10:03:07 +01:00
import android.widget.Button;
2025-12-10 14:00:27 +01:00
import android.widget.CheckBox;
2025-12-10 10:03:07 +01:00
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
2025-12-10 14:00:27 +01:00
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ScrollView;
import android.widget.Spinner;
2025-12-10 10:03:07 +01:00
import android.widget.TextView;
import android.widget.Toast;
2025-12-10 14:00:27 +01:00
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
2025-12-10 10:03:07 +01:00
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
2025-12-10 14:00:27 +01:00
import com.google.gson.JsonElement;
2025-12-11 10:18:19 +01:00
import com.google.gson.JsonObject;
2025-12-10 14:00:27 +01:00
2025-12-10 10:03:07 +01:00
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
2025-12-10 14:00:27 +01:00
import java.io.InputStream;
2025-12-10 10:03:07 +01:00
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
2025-12-10 14:00:27 +01:00
import java.text.SimpleDateFormat;
2025-12-10 10:03:07 +01:00
import java.util.ArrayList;
2025-12-10 14:00:27 +01:00
import java.util.Calendar;
2025-12-10 10:03:07 +01:00
import java.util.Collections;
2025-12-10 14:00:27 +01:00
import java.util.Date;
2025-12-10 10:03:07 +01:00
import java.util.HashMap;
import java.util.List;
2025-12-10 14:00:27 +01:00
import java.util.Locale;
2025-12-10 10:03:07 +01:00
import java.util.Map;
2025-12-10 14:00:27 +01:00
import java.util.regex.Matcher;
import java.util.regex.Pattern;
2025-12-10 10:03:07 +01:00
import okhttp3.Call;
import okhttp3.MediaType;
2025-12-10 14:00:27 +01:00
import okhttp3.MultipartBody;
2025-12-10 10:03:07 +01:00
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
2025-12-10 14:00:27 +01:00
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
2025-12-10 10:03:07 +01:00
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";
2025-12-10 14:00:27 +01:00
// SKJEMA ID-er
2025-12-10 10:03:07 +01:00
private static final int ID_ANSATTEOPPLYSNINGER = 1;
2025-12-10 14:00:27 +01:00
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;
2025-12-10 10:03:07 +01:00
private int formId = 1;
private LinearLayout formContainer;
private LinearLayout historyContainer;
private TextView txtStatus;
2025-12-10 14:00:27 +01:00
private TextView lblHistory;
2025-12-10 10:03:07 +01:00
private ProgressBar loadingSpinner;
2025-12-10 14:00:27 +01:00
// --- HOVEDSKJEMA STATE ---
private Map<String, View> fieldWrappers = new HashMap<>();
private Map<String, View> inputViews = new HashMap<>();
2025-12-10 10:03:07 +01:00
private Map<String, Boolean> requiredFieldsMap = new HashMap<>();
2025-12-10 14:00:27 +01:00
private Map<String, Uri> fileUploads = new HashMap<>();
// --- NESTED FORM (BARN) STATE ---
private Map<String, View> childInputViews = new HashMap<>();
private Map<String, Boolean> childRequiredFieldsMap = new HashMap<>();
private Map<String, Uri> childFileUploads = new HashMap<>();
// Lagring av Nested Entries
private List<NestedEntry> nestedEntries = new ArrayList<>();
private LinearLayout nestedEntriesContainer;
private TextView totalAmountView;
private String pendingFileFieldId = null;
private boolean isSelectingForChild = false;
private ActivityResultLauncher<Intent> filePickerLauncher;
private GravityForm currentForm;
2025-12-10 10:03:07 +01:00
private final OkHttpClient client = new OkHttpClient();
2025-12-10 14:00:27 +01:00
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);
}
}
pendingFileFieldId = null;
isSelectingForChild = false;
}
);
}
2025-12-10 10:03:07 +01:00
@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);
if (formContainer == null) {
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<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> response) {
if (response.isSuccessful() && response.body() != null) {
2025-12-10 14:00:27 +01:00
currentForm = response.body();
2025-12-10 10:03:07 +01:00
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
2025-12-10 14:00:27 +01:00
renderDynamicForm(currentForm);
2025-12-10 10:03:07 +01:00
fetchFormEntries();
});
}
} else {
updateStatus("Feil ved lasting av skjema.");
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
}
}
@Override
public void onFailure(retrofit2.Call<GravityForm> 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();
2025-12-10 14:00:27 +01:00
fieldWrappers.clear();
inputViews.clear();
2025-12-10 10:03:07 +01:00
requiredFieldsMap.clear();
2025-12-10 14:00:27 +01:00
fileUploads.clear();
nestedEntries.clear();
2025-12-10 10:03:07 +01:00
updateStatus("");
TextView title = new TextView(getContext());
2025-12-10 14:00:27 +01:00
title.setText(getCleanTitle(form.title));
2025-12-10 10:03:07 +01:00
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());
2025-12-10 14:00:27 +01:00
String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", "");
formDesc.setText(cleanDesc);
2025-12-10 10:03:07 +01:00
formDesc.setPadding(0, 0, 0, 40);
formContainer.addView(formDesc);
}
if (form.fields == null) return;
for (GravityField field : form.fields) {
2025-12-10 14:00:27 +01:00
if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) {
continue;
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
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);
2025-12-10 10:03:07 +01:00
if ("section".equals(field.type)) {
2025-12-10 14:00:27 +01:00
addSectionHeader(fieldWrapper, field.label, field.description);
formContainer.addView(fieldWrapper);
2025-12-10 10:03:07 +01:00
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));
2025-12-10 14:00:27 +01:00
fieldWrapper.addView(htmlView);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
formContainer.addView(fieldWrapper);
2025-12-10 10:03:07 +01:00
continue;
}
TextView label = new TextView(getContext());
String labelText = field.label;
if (field.isRequired) labelText += " *";
label.setText(labelText);
label.setTextColor(Color.DKGRAY);
2025-12-10 14:00:27 +01:00
label.setTypeface(null, Typeface.BOLD);
label.setPadding(0, 10, 0, 5);
fieldWrapper.addView(label);
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
if ("form".equals(field.type)) {
renderNestedFormField(fieldWrapper, field);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
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);
2025-12-10 10:03:07 +01:00
} else {
2025-12-10 14:00:27 +01:00
renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap);
2025-12-10 10:03:07 +01:00
}
if (field.description != null && !field.description.isEmpty()) {
2025-12-10 14:00:27 +01:00
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);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
formContainer.addView(fieldWrapper);
2025-12-10 10:03:07 +01:00
}
Button dynamicSubmit = new Button(getContext());
dynamicSubmit.setText("Send inn skjema");
dynamicSubmit.setTextColor(Color.WHITE);
dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå
2025-12-10 14:00:27 +01:00
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2025-12-10 10:03:07 +01:00
params.setMargins(0, 60, 0, 20);
dynamicSubmit.setLayoutParams(params);
dynamicSubmit.setOnClickListener(v -> submitDynamicForm());
formContainer.addView(dynamicSubmit);
2025-12-10 14:00:27 +01:00
evaluateAllConditionalLogic();
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
// --- 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 -> {
int childFormId = 18;
if (field.gpnfForm != null) {
try {
childFormId = Integer.parseInt(field.gpnfForm);
} catch (NumberFormatException e) { e.printStackTrace(); }
}
openChildFormDialog(childFormId);
});
container.addView(btnAdd);
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
EditText hiddenIds = new EditText(getContext());
hiddenIds.setVisibility(View.GONE);
inputViews.put(field.id, hiddenIds);
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
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);
}
private void openChildFormDialog(int childFormId) {
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<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> response) {
loadingDialog.dismiss();
if (response.isSuccessful() && response.body() != null) {
showChildFormDialog(response.body());
} else {
Toast.makeText(getContext(), "Kunne ikke hente underskjema", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call<GravityForm> call, Throwable t) {
loadingDialog.dismiss();
Toast.makeText(getContext(), "Feil: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private void showChildFormDialog(GravityForm childForm) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
2025-12-11 10:18:19 +01:00
// FJERNET: builder.setTitle(childForm.title); - Brukeren ønsket ikke tittel i popup
2025-12-10 14:00:27 +01:00
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);
});
}
private void submitChildForm(int childFormId, AlertDialog dialog) {
JSONObject inputValues = new JSONObject();
for (Map.Entry<String, View> 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<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> 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));
}
}
for (Map.Entry<String, Uri> 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<JsonElement>() {
@Override
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> response) {
2025-12-11 10:18:19 +01:00
if (response.isSuccessful() && response.body() != null) {
2025-12-10 14:00:27 +01:00
try {
2025-12-11 10:18:19 +01:00
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
2025-12-10 14:00:27 +01:00
String desc = getInputValueGeneric(childInputViews.get("3"));
String price = getInputValueGeneric(childInputViews.get("4"));
2025-12-11 10:18:19 +01:00
2025-12-10 14:00:27 +01:00
addNestedEntry(entryId, desc, price);
dialog.dismiss();
2025-12-11 10:18:19 +01:00
} else {
Toast.makeText(getContext(), "Ugyldig respons fra server", Toast.LENGTH_SHORT).show();
2025-12-10 14:00:27 +01:00
}
2025-12-11 10:18:19 +01:00
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(getContext(), "Feil ved parsing av svar", Toast.LENGTH_SHORT).show();
}
2025-12-10 14:00:27 +01:00
} else {
Toast.makeText(getContext(), "Feil ved opplasting", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call<JsonElement> 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<String> 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 ---
private void renderFileUploadField(LinearLayout container, GravityField field, Map<String, View> viewsMap, Map<String, Boolean> reqMap, boolean isChild) {
LinearLayout fileLayout = new LinearLayout(getContext());
fileLayout.setOrientation(LinearLayout.HORIZONTAL);
Button btnUpload = new Button(getContext());
btnUpload.setText("Velg fil");
btnUpload.setOnClickListener(v -> {
if (filePickerLauncher == null) return;
try {
pendingFileFieldId = field.id;
isSelectingForChild = isChild;
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);
} catch (Exception e) {
Toast.makeText(getContext(), "Kunne ikke åpne filvelger", Toast.LENGTH_SHORT).show();
}
});
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);
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
private void handleFileSelection(String fieldId, Uri uri, boolean isChild) {
if (isChild) {
childFileUploads.put(fieldId, uri);
} else {
fileUploads.put(fieldId, uri);
}
Map<String, View> 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);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
} 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<String, View> views, Map<String, Boolean> req) {
EditText timeInput = new EditText(getContext());
timeInput.setFocusable(false);
timeInput.setClickable(true);
timeInput.setHint("00:00");
timeInput.setOnClickListener(v -> {
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<String, View> views, Map<String, Boolean> 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(); }
});
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderTextAreaField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> 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);
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderRadioField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> 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);
group.addView(rb);
}
}
group.setOnCheckedChangeListener((g, i) -> evaluateAllConditionalLogic());
container.addView(group);
views.put(field.id, group);
req.put(field.id, field.isRequired);
}
private void renderSelectField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
Spinner spinner = new Spinner(getContext());
List<String> labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) {
for (GravityField.Choice c : field.choices) labels.add(c.text);
}
ArrayAdapter<String> 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<String, View> views, Map<String, Boolean> 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;
2025-12-11 10:18:19 +01:00
// For Consent fields, Gravity Forms typically expects "1" if checked.
checkBox.setTag("1");
2025-12-10 14:00:27 +01:00
checkBox.setOnCheckedChangeListener((b, c) -> evaluateAllConditionalLogic());
container.addView(checkBox);
views.put(inputId, checkBox);
req.put(inputId, field.isRequired);
}
private void renderCheckboxField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
if (field.inputs != null) {
2025-12-11 10:18:19 +01:00
for (int i = 0; i < field.inputs.size(); i++) {
GravityField inputDef = field.inputs.get(i);
2025-12-10 14:00:27 +01:00
CheckBox checkBox = new CheckBox(getContext());
checkBox.setText(inputDef.label);
2025-12-11 10:18:19 +01:00
// --- VIKTIG ENDRING: HENT KORREKT VERDI ---
// Gravity Forms sjekkbokser trenger verdien fra "choices" listen, ikke bare "1".
// F.eks: "Ja" hvis valget er "Ja".
String value = "1"; // Fallback
if (field.choices != null && i < field.choices.size()) {
value = field.choices.get(i).value;
}
checkBox.setTag(value);
// -------------------------------------------
2025-12-10 14:00:27 +01:00
checkBox.setOnCheckedChangeListener((b, c) -> evaluateAllConditionalLogic());
container.addView(checkBox);
views.put(inputDef.id, checkBox);
req.put(inputDef.id, false);
}
}
}
private void renderDateField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> 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 -> {
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();
2025-12-10 10:03:07 +01:00
});
}
2025-12-10 14:00:27 +01:00
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<String, View> views, Map<String, Boolean> req) {
UserManager user = UserManager.getInstance();
boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER);
List<GravityField> inputs = new ArrayList<>(parentField.inputs);
if ("address".equals(parentField.type)) {
Collections.sort(inputs, (f1, f2) -> Integer.compare(getAddressScore(f1.label), getAddressScore(f2.label)));
}
2025-12-10 10:03:07 +01:00
for (GravityField subField : inputs) {
2025-12-10 14:00:27 +01:00
if (subField.isHidden || "hidden".equals(subField.visibility)) continue;
2025-12-10 10:03:07 +01:00
TextView subLabel = new TextView(getContext());
String subLabelText = subField.label;
boolean isSubRequired = parentField.isRequired;
2025-12-10 14:00:27 +01:00
if ("address".equals(parentField.type) && subField.id.endsWith(".2")) {
2025-12-10 10:03:07 +01:00
isSubRequired = false;
}
if (isSubRequired) subLabelText += " *";
subLabel.setText(subLabelText);
subLabel.setTextColor(Color.GRAY);
subLabel.setTextSize(12);
subLabel.setPadding(0, 10, 0, 0);
2025-12-10 14:00:27 +01:00
container.addView(subLabel);
2025-12-10 10:03:07 +01:00
EditText subInput = new EditText(getContext());
subInput.setPadding(30, 30, 30, 30);
subInput.setBackgroundResource(android.R.drawable.edit_text);
subInput.setInputType(InputType.TYPE_CLASS_TEXT);
2025-12-10 14:00:27 +01:00
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());
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
container.addView(subInput);
views.put(subField.id, subInput);
req.put(subField.id, isSubRequired);
2025-12-10 10:03:07 +01:00
}
}
2025-12-10 14:00:27 +01:00
private void addSectionHeader(LinearLayout container, String title, String descText) {
2025-12-10 10:03:07 +01:00
TextView sectionHeader = new TextView(getContext());
sectionHeader.setText(title);
sectionHeader.setTextSize(18);
sectionHeader.setTypeface(null, Typeface.BOLD);
sectionHeader.setTextColor(Color.parseColor("#0069B3"));
2025-12-10 14:00:27 +01:00
sectionHeader.setPadding(0, 20, 0, 5);
container.addView(sectionHeader);
2025-12-10 10:03:07 +01:00
if (descText != null && !descText.isEmpty()) {
2025-12-10 14:00:27 +01:00
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);
2025-12-10 10:03:07 +01:00
}
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2));
line.setBackgroundColor(Color.LTGRAY);
2025-12-10 14:00:27 +01:00
line.setPadding(0,0,0,20);
container.addView(line);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
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;
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
private void evaluateAllConditionalLogic() {
if (currentForm == null || currentForm.fields == null) return;
for (GravityField field : currentForm.fields) {
if (field.conditionalLogic == null) {
setViewVisibility(field.id, true);
continue;
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
boolean isMatch = evaluateLogic(field.conditionalLogic);
boolean show = "show".equalsIgnoreCase(field.conditionalLogic.actionType);
boolean shouldBeVisible = (show && isMatch) || (!show && !isMatch);
setViewVisibility(field.id, shouldBeVisible);
}
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
private boolean evaluateLogic(GravityField.ConditionalLogic logic) {
if (logic.rules == null || logic.rules.isEmpty()) return true;
boolean isAll = "all".equalsIgnoreCase(logic.logicType);
boolean aggregatedResult = isAll;
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
for (GravityField.Rule rule : logic.rules) {
String val = getInputValue(rule.fieldId);
boolean ruleMatch = checkRule(val, rule.operator, rule.value);
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
if (isAll) {
aggregatedResult = aggregatedResult && ruleMatch;
if (!aggregatedResult) break;
} else {
aggregatedResult = aggregatedResult || ruleMatch;
if (aggregatedResult) break;
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
}
return aggregatedResult;
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
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;
}
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
private String getInputValue(String fieldId) {
View view = inputViews.get(fieldId);
return getInputValueGeneric(view);
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
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();
2025-12-10 10:03:07 +01:00
}
}
2025-12-10 14:00:27 +01:00
if (view instanceof Spinner) {
if (((Spinner) view).getSelectedItemPosition() == 0) return "";
Object item = ((Spinner) view).getSelectedItem();
return item != null ? item.toString() : "";
}
2025-12-11 10:18:19 +01:00
// --- VIKTIG ENDRING: HENT TAG-VERDI ---
if (view instanceof CheckBox) {
CheckBox cb = (CheckBox) view;
if (cb.isChecked()) {
// Returner verdien lagret i Tag (f.eks "Ja") i stedet for hardkodet "1"
return cb.getTag() != null ? cb.getTag().toString() : "1";
}
return "";
}
// ----------------------------------------
2025-12-10 14:00:27 +01:00
return "";
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
private void setViewVisibility(String fieldId, boolean visible) {
View wrapper = fieldWrappers.get(fieldId);
if (wrapper != null) {
wrapper.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
// --- SUBMISSION ---
2025-12-10 10:03:07 +01:00
private void submitDynamicForm() {
JSONObject inputValues = new JSONObject();
boolean hasValues = false;
2025-12-10 14:00:27 +01:00
Log.d(TAG, "submitDynamicForm: Starting validation...");
for (Map.Entry<String, View> entry : inputViews.entrySet()) {
2025-12-10 10:03:07 +01:00
String fieldId = entry.getKey();
2025-12-10 14:00:27 +01:00
View view = entry.getValue();
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
View wrapper = fieldWrappers.get(fieldId);
if (wrapper == null) {
if (!view.isShown()) continue;
} else {
if (wrapper.getVisibility() != View.VISIBLE) continue;
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
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()) {
2025-12-10 10:03:07 +01:00
try {
2025-12-10 14:00:27 +01:00
// --- DATO FORMATERING FOR API (Fix 400 Bad Request) ---
GravityField fieldDef = getGravityFieldById(fieldId);
if (fieldDef != null && "date".equals(fieldDef.type)) {
val = formatDateForApi(val);
}
inputValues.put("input_" + fieldId, val);
2025-12-10 10:03:07 +01:00
hasValues = true;
2025-12-10 14:00:27 +01:00
} catch (JSONException e) {}
2025-12-10 10:03:07 +01:00
}
}
2025-12-10 14:00:27 +01:00
if (!hasValues && fileUploads.isEmpty()) {
Log.d(TAG, "Submit aborted: Form is empty");
2025-12-10 10:03:07 +01:00
Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show();
return;
}
updateStatus("Sender inn...");
String cookie = UserManager.getInstance().getCookie();
2025-12-10 14:00:27 +01:00
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(Call call, IOException e) {
Log.e(TAG, "JSON submit failed", e);
updateStatus("Feil: " + e.getMessage());
}
public void onResponse(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 {
2025-12-11 10:18:19 +01:00
// --- BEDRE LOGGING FOR 400 FEIL ---
String errBody = response.body() != null ? response.body().string() : "No body";
2025-12-10 14:00:27 +01:00
Log.e(TAG, "Server error body: " + errBody);
2025-12-11 10:18:19 +01:00
updateStatus("Feil (" + response.code() + "): " + errBody); // Viser feilmeldingen i UI
2025-12-10 14:00:27 +01:00
} catch(Exception e){}
}
}
});
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
}
// Konverterer dd.MM.yyyy -> yyyy-MM-dd
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; // Fallback hvis formatet er ukjent
}
}
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<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for(int i=0; i<names.length(); i++) {
String k = names.getString(i);
textParts.put(k, RequestBody.create(MultipartBody.FORM, inputValues.getString(k)));
}
}
for (Map.Entry<String, Uri> 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<JsonElement>() {
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> 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<JsonElement> 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() {
2025-12-10 10:03:07 +01:00
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
2025-12-10 14:00:27 +01:00
Log.e(TAG, "Kunne ikke hente historikk", e);
2025-12-10 10:03:07 +01:00
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
2025-12-10 14:00:27 +01:00
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(); }
2025-12-10 10:03:07 +01:00
}
}
});
}
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 {
2025-12-10 14:00:27 +01:00
int count = Math.min(entries.length(), 5);
for (int i = 0; i < count; i++) {
2025-12-10 10:03:07 +01:00
JSONObject entry = entries.getJSONObject(i);
String date = entry.optString("date_created");
TextView item = new TextView(getContext());
2025-12-10 14:00:27 +01:00
item.setText("Innsendt: " + date);
2025-12-10 10:03:07 +01:00
item.setPadding(10, 20, 10, 20);
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);
2025-12-10 14:00:27 +01:00
2025-12-10 10:03:07 +01:00
historyContainer.addView(item);
historyContainer.addView(line);
}
} catch (JSONException e) { e.printStackTrace(); }
}
2025-12-10 14:00:27 +01:00
private void prefillFormFromHistory(JSONObject latestEntry) {
if (latestEntry == null) return;
for (Map.Entry<String, View> 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()) {
2025-12-11 10:18:19 +01:00
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) {
// Fix: CheckBox inherits from Button, so we must be careful.
// Only cast to TextView if the tag is actually a TextView (File Upload logic)
Object tag = view.getTag();
if (tag instanceof TextView) {
((TextView) tag).setText("Ingen fil valgt");
}
}
2025-12-10 14:00:27 +01:00
}
fileUploads.clear();
nestedEntries.clear();
if (nestedEntriesContainer != null) nestedEntriesContainer.removeAllViews();
if (totalAmountView != null) totalAmountView.setText("Kr 0,00");
}
// --- HJELPEKLASSE FOR NESTED ENTRIES ---
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; }
}
2025-12-10 10:03:07 +01:00
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;
2025-12-10 14:00:27 +01:00
import android.util.Log;
2025-12-10 10:03:07 +01:00
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;
2025-12-10 14:00:27 +01:00
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
2025-12-10 10:03:07 +01:00
import java.util.Map;
2025-12-10 14:00:27 +01:00
import java.util.regex.Matcher;
import java.util.regex.Pattern;
2025-12-10 10:03:07 +01:00
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class FormsListFragment extends Fragment {
2025-12-10 14:00:27 +01:00
private static final String TAG = "FormsListFragment";
2025-12-10 10:03:07 +01:00
private LinearLayout container;
private ProgressBar loadingSpinner;
private TextView txtStatus;
2025-12-10 14:00:27 +01:00
// Regex for å finne tall i starten av tittelen (f.eks "10. Tittel")
// Group 1 = Tallet
// Group 2 = Resten av teksten (den rene tittelen)
private static final Pattern TITLE_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)");
2025-12-10 10:03:07 +01:00
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
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);
2025-12-10 14:00:27 +01:00
View historyTitle = view.findViewById(R.id.historyContainer);
2025-12-10 10:03:07 +01:00
if (historyTitle != null) historyTitle.setVisibility(View.GONE);
LinearLayout mainLayout = view.findViewById(R.id.main_layout);
if (mainLayout != null && mainLayout.getChildCount() > 4) {
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("");
2025-12-10 14:00:27 +01:00
// Vi bruker standard-kallet som er raskt og effektivt
2025-12-10 10:03:07 +01:00
RetrofitClient.getApiService().getFormsListMap().enqueue(new Callback<Map<String, GravityForm>>() {
@Override
public void onResponse(Call<Map<String, GravityForm>> call, Response<Map<String, GravityForm>> response) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (response.isSuccessful() && response.body() != null) {
Map<String, GravityForm> formMap = response.body();
if (formMap.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet.");
return;
}
2025-12-10 14:00:27 +01:00
List<GravityForm> visibleForms = new ArrayList<>();
for (GravityForm form : formMap.values()) {
// Sjekk om aktiv
boolean isActive = form.isActive == null || !"0".equals(form.isActive);
// Sjekk om vi finner et sorteringstall i tittelen
Integer sortOrder = getSortOrderFromTitle(form.title);
if (form != null && form.title != null && isActive && sortOrder != null) {
visibleForms.add(form);
}
}
if (visibleForms.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen tilgjengelige skjemaer (sjekk nummerering i titler).");
return;
}
2025-12-10 10:03:07 +01:00
2025-12-10 14:00:27 +01:00
// Sorter listen basert på tallet i tittelen
Collections.sort(visibleForms, new Comparator<GravityForm>() {
@Override
public int compare(GravityForm f1, GravityForm f2) {
return Integer.compare(getSortOrderFromTitle(f1.title), getSortOrderFromTitle(f2.title));
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
});
for (GravityForm form : visibleForms) {
addFormButton(form);
2025-12-10 10:03:07 +01:00
}
2025-12-10 14:00:27 +01:00
2025-12-10 10:03:07 +01:00
} else {
if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")");
}
}
@Override
public void onFailure(Call<Map<String, GravityForm>> call, Throwable t) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage());
}
});
}
2025-12-10 14:00:27 +01:00
/**
* Henter ut tallet fra starten av tittelen.
* Returnerer null hvis tittelen ikke starter med et tall.
*/
private Integer getSortOrderFromTitle(String title) {
if (title == null) return null;
Matcher m = TITLE_PATTERN.matcher(title.trim());
if (m.find()) {
try {
return Integer.parseInt(m.group(1));
} catch (NumberFormatException e) {
return null;
}
}
return null; // Ingen tall funnet -> Skjules
}
/**
* Henter ut selve navnet på skjemaet (uten tallet foran).
* "10. Ansatte" -> "Ansatte"
*/
private String getCleanTitle(String title) {
if (title == null) return "";
Matcher m = TITLE_PATTERN.matcher(title.trim());
if (m.find()) {
// Group 2 er resten av teksten etter tallet og punktum/mellomrom
return m.group(2);
}
return title; // Fallback hvis regex ikke matcher (skal ikke skje pga filtreringen over)
}
2025-12-10 10:03:07 +01:00
private void addFormButton(GravityForm form) {
Button button = new Button(getContext());
2025-12-10 14:00:27 +01:00
// VIS VASKET TITTEL PÅ KNAPPEN
String displayTitle = getCleanTitle(form.title);
button.setText(displayTitle.toUpperCase());
2025-12-10 10:03:07 +01:00
button.setTextColor(Color.WHITE);
button.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå
button.setTextSize(14);
button.setPadding(30, 30, 30, 30);
button.setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putInt("formId", form.id);
Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle);
});
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
2025-12-10 14:00:27 +01:00
params.setMargins(0, 0, 0, 20);
2025-12-10 10:03:07 +01:00
button.setLayoutParams(params);
container.addView(button);
}
}
============================================================
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<String, String> inputValues;
public FormSubmission(Map<String, String> 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<Map<String, String>> entries;
// Vi bruker Map<String, String> 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;
2025-12-10 14:00:27 +01:00
@SerializedName("inputType")
public String inputType;
2025-12-10 10:03:07 +01:00
@SerializedName("label")
public String label;
2025-12-10 14:00:27 +01:00
@SerializedName("adminLabel")
public String adminLabel;
2025-12-10 10:03:07 +01:00
@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<Choice> choices;
@SerializedName("content")
public String content;
@SerializedName("inputs")
public List<GravityField> inputs;
@SerializedName("isHidden")
public boolean isHidden;
2025-12-10 14:00:27 +01:00
// NYTT: For å sjekke om feltet er Read Only (f.eks dato i refusjon)
@SerializedName("gwreadonly_enable")
public boolean readOnly;
2025-12-10 10:03:07 +01:00
@JsonAdapter(ConditionalLogicAdapter.class)
@SerializedName("conditionalLogic")
public ConditionalLogic conditionalLogic;
@SerializedName("gppa-values-templates")
public java.util.Map<String, String> gppaTemplates;
2025-12-10 14:00:27 +01:00
@SerializedName("gpnfForm")
public String gpnfForm;
2025-12-10 10:03:07 +01:00
public static class Choice {
@SerializedName("text")
public String text;
@SerializedName("value")
public String value;
}
public static class ConditionalLogic {
@SerializedName("actionType")
2025-12-10 14:00:27 +01:00
public String actionType;
2025-12-10 10:03:07 +01:00
@SerializedName("logicType")
2025-12-10 14:00:27 +01:00
public String logicType;
2025-12-10 10:03:07 +01:00
@SerializedName("rules")
public List<Rule> rules;
}
public static class Rule {
@SerializedName("fieldId")
public String fieldId;
@SerializedName("operator")
2025-12-10 14:00:27 +01:00
public String operator;
2025-12-10 10:03:07 +01:00
@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;
2025-12-10 14:00:27 +01:00
@SerializedName("is_active")
public String isActive; // "1" = Aktiv, "0" = Inaktiv
2025-12-10 10:03:07 +01:00
@SerializedName("fields")
2025-12-10 14:00:27 +01:00
public List<GravityField> fields;
2025-12-10 10:03:07 +01:00
}
============================================================
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
============================================================
// 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<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (getContext() == null || response.body() == null) return;
List<CalendarEvent> rawEvents = response.body();
List<CalendarEvent> 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<List<CalendarEvent>> call, Throwable t) {
if (getContext() == null) return;
System.err.println("Kalender Nettverksfeil: " + t.getMessage());
// Vis feilmelding i RecyclerView
List<CalendarEvent> 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<List<WpPost>>() {
@Override
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> 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<WpPost> wpPosts = response.body();
List<NewsItem> 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<List<WpPost>> 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<NewsItem> errorList = new ArrayList<>();
errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System"));
recyclerView.setAdapter(new NewsAdapter(errorList));
}
});
}
}
============================================================
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<Intent> signInLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
Task<GoogleSignInAccount> 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<GoogleSignInAccount> 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
2025-12-10 14:00:27 +01:00
public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com";
2025-12-10 10:03:07 +01:00
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.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private List<NewsItem> newsList;
public NewsAdapter(List<NewsItem> 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);
}
}
}
============================================================
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\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 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<List<GravityField.Choice>>(){}.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;
}
}
============================================================
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
============================================================
// 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<List<WpPost>> getPosts();
// 2. Hent et spesifikt skjema med ID
@GET("wp-json/gf/v2/forms/{id}")
Call<GravityForm> getForm(@Path("id") int formId);
// 3. SEND INN SKJEMA (JSON-data uten filer)
@POST("wp-json/gf/v2/forms/{id}/submissions")
Call<JsonElement> submitForm(@Path("id") int formId, @Body FormSubmission submission);
// 4. LOGIN MED GOOGLE
@POST("wp-json/kbs/v1/login")
Call<LoginResponse> googleLogin(@Body LoginRequest request);
// 5. HENT LISTE AV SKJEMAER
@GET("wp-json/gf/v2/forms")
Call<Map<String, GravityForm>> getFormsListMap();
// 6. SEND INN SKJEMA (MULTIPART - for filopplasting)
@Multipart
@POST("wp-json/gf/v2/forms/{id}/submissions")
Call<JsonElement> submitMultipartForm(
@Path("id") int formId,
@PartMap Map<String, RequestBody> textFields,
@Part List<MultipartBody.Part> files
);
// 7. HENT KALENDERHENDELSER
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
// 8. HENT INNSENDINGER (Entries) - NYTT
@GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId,
@Query("search") String searchJson,
@Query("paging[page_size]") int pageSize
);
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\WpPost.java
============================================================
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 <p>)
return excerpt != null ? android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString() : "";
}
}
============================================================
FILSTI: app\src\main\res\drawable\ic_book.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_form.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M22,7h-9v2h9V7zM22,15h-9v2h9V15zM5.54,11L2,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,11zM5.54,19L2,15.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,19z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_home.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_launcher_background.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
============================================================
FILSTI: app\src\main\res\layout\activity_main.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:menu="@menu/bottom_nav_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_forms.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@android:color/white">
<LinearLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjema"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
<TextView
android:id="@+id/lbl_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tidligere innsendinger:"
android:textStyle="bold"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"/>
<LinearLayout
android:id="@+id/historyContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="20dp"/>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#0069B3"
android:layout_marginBottom="20dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ny innsending:"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="10dp"/>
<LinearLayout
android:id="@+id/form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<TextView
android:id="@+id/txt_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="#D32F2F"
android:gravity="center" />
</LinearLayout>
</ScrollView>
============================================================
FILSTI: app\src\main\res\layout\fragment_handbook.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Håndbok"
android:layout_gravity="center"
android:textSize="24sp"/>
</FrameLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_home.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:background="@color/kbs_very_light_blue"> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingHorizontal="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KBS Intranett"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray" android:layout_centerVertical="true"/>
<ImageView
android:id="@+id/btn_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_my_calendar"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
app:tint="@color/kbs_logo_blue"/> </RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kommende hendelser"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_calendar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Siste nytt"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_news"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:scrollbars="vertical"/>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_login.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:background="#FFFFFF">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KBS Intranett"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Vennligst logg inn"
android:textSize="16sp"
android:textColor="#666666"
android:layout_marginBottom="48dp"/>
<com.google.android.gms.common.SignInButton
android:id="@+id/sign_in_button"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text=""
android:textColor="#D32F2F"/>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_profile.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/kbs_very_light_blue"> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:paddingHorizontal="10dp"
android:background="@color/kbs_logo_blue" > <ImageView
android:id="@+id/btn_close_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
app:tint="@color/white"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min Profil"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/white"
android:layout_centerInParent="true"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingTop="32dp"> <ImageView
android:id="@+id/profile_image"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@android:drawable/sym_def_app_icon"
android:background="@android:color/transparent"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Laster navn..."
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray"/>
<TextView
android:id="@+id/profile_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="..."
android:textSize="16sp"
android:textColor="@color/kbs_muted_blue_gray"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/profile_role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rolle: ..."
android:textSize="14sp"
android:textStyle="italic"
android:textColor="@color/kbs_logo_blue"
android:background="@color/kbs_very_light_blue" android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="48dp"/>
<Button
android:id="@+id/btn_logout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Logg ut"
android:backgroundTint="@color/kbs_logo_accent_red" android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\item_calendar.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginHorizontal="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="50dp"
android:layout_height="50dp"
android:orientation="vertical"
android:gravity="center"
android:background="@color/kbs_logo_light_blue" android:layout_marginEnd="16dp">
<TextView
android:id="@+id/cal_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/white"/>
<TextView
android:id="@+id/cal_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DES"
android:textSize="10sp"
android:textAllCaps="true"
android:textColor="@color/white"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/cal_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Møtetittel"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray"/>
<TextView
android:id="@+id/cal_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Kl. 10:00 - 11:00"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
============================================================
FILSTI: app\src\main\res\layout\item_news.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Nyhets overskrift"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black"/>
<TextView
android:id="@+id/news_excerpt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Her kommer ingressen..."
android:textColor="@android:color/darker_gray"/>
<TextView
android:id="@+id/news_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Skrevet av: Ole"
android:textSize="12sp"
android:textStyle="italic"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
============================================================
FILSTI: app\src\main\res\menu\bottom_nav_menu.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@android:drawable/ic_menu_today"
android:title="Hjem" />
<item
android:id="@+id/navigation_forms"
android:icon="@android:drawable/ic_menu_edit"
android:title="Skjema" />
<item
android:id="@+id/navigation_handbook"
android:icon="@android:drawable/ic_menu_info_details"
android:title="Håndbok" />
</menu>
============================================================
FILSTI: app\src\main\res\mipmap-anydpi-v26\ic_launcher.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
============================================================
FILSTI: app\src\main\res\mipmap-anydpi-v26\ic_launcher_round.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
============================================================
FILSTI: app\src\main\res\navigation\mobile_navigation.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_login"
android:name="com.kbs.kbsintranett.LoginFragment"
android:label="Logg inn"
tools:layout="@layout/fragment_login">
<action
android:id="@+id/action_login_to_home"
app:destination="@id/navigation_home"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/navigation_home"
android:name="com.kbs.kbsintranett.HomeFragment"
android:label="Hjem"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_forms"
android:name="com.kbs.kbsintranett.FormsListFragment"
android:label="Skjemaer"
tools:layout="@layout/fragment_forms">
<action
android:id="@+id/action_formsListFragment_to_formsDetailFragment"
app:destination="@id/navigation_forms_detail" />
</fragment>
<fragment
android:id="@+id/navigation_forms_detail"
android:name="com.kbs.kbsintranett.FormsFragment"
android:label="Fyll ut skjema"
tools:layout="@layout/fragment_forms" >
<argument
android:name="formId"
app:argType="integer"
android:defaultValue="0" />
</fragment>
<fragment
android:id="@+id/navigation_handbook"
android:name="com.kbs.kbsintranett.HandbookFragment"
android:label="Håndbok"
tools:layout="@layout/fragment_handbook" />
<fragment
android:id="@+id/navigation_profile"
android:name="com.kbs.kbsintranett.ProfileFragment"
android:label="Min Profil"
tools:layout="@layout/fragment_profile">
<action
android:id="@+id/action_profile_to_login"
app:destination="@id/navigation_login"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="true" />
</fragment>
</navigation>
============================================================
FILSTI: app\src\main\res\values\colors.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="kbs_logo_blue">#0069B3</color>
<color name="kbs_logo_light_blue">#53AFE9</color>
<color name="kbs_logo_accent_red">#C40426</color>
<color name="kbs_very_light_blue">#F5F7FA</color>
<color name="kbs_muted_blue_gray">#4F5B66</color>
<color name="kbs_soft_light_pink_beige">#F8E5E8</color>
</resources>
============================================================
FILSTI: app\src\main\res\values\ic_launcher_background.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
============================================================
FILSTI: app\src\main\res\values\strings.xml
============================================================
<resources>
<string name="app_name">KBS Intranett</string>
</resources>
============================================================
FILSTI: app\src\main\res\values\themes.xml
============================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Base.Theme.KBSIntranett" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">#0056b3</item> <item name="colorPrimaryVariant">#004494</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.KBSIntranett" parent="Base.Theme.KBSIntranett" />
</resources>
============================================================
FILSTI: app\src\main\res\values-night\themes.xml
============================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Base.Theme.KBSIntranett" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">#0056b3</item> <item name="colorPrimaryVariant">#004494</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.KBSIntranett" parent="Base.Theme.KBSIntranett" />
</resources>
============================================================
FILSTI: app\src\main\res\xml\backup_rules.xml
============================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
============================================================
FILSTI: app\src\main\res\xml\data_extraction_rules.xml
============================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
============================================================
FILSTI: app\src\test\java\com\kbs\kbsintranett\ExampleUnitTest.java
============================================================
package com.kbs.kbsintranett;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}