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 Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.kbs.kbsintranett", appContext.getPackageName());
}
}
============================================================
FILSTI: app\src\main\AndroidManifest.xml
============================================================
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
============================================================
// FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
package com.kbs.kbsintranett;
import android.util.Log;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AuthRepository {
private static final String TAG = "AuthRepository";
// Interface for å gi beskjed tilbake til Activity/Fragment
public interface AuthCallback {
void onSuccess(String role);
void onError(String message);
}
/**
* Utfører selve API-kallet mot WordPress.
* Denne brukes nå av både MainActivity (Silent Sign-In) og LoginFragment (Manuell).
*/
public static void loginToWordPress(String googleIdToken, String displayName, String email, String photoUrl, AuthCallback callback) {
// 1. Lagre Google-info midlertidig
UserManager.getInstance().setUserData(displayName, email, googleIdToken, photoUrl);
// 2. Gjør klar request
LoginRequest request = new LoginRequest(googleIdToken);
// 3. Send til WordPress
RetrofitClient.getApiService().googleLogin(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
if (response.isSuccessful() && response.body() != null && response.body().success) {
// SUKSESS!
String cookie = response.body().fullCookie;
String role = response.body().role;
// NYTT: Hent utvidet info fra responsen
int userId = response.body().userId;
String fName = response.body().firstName;
String lName = response.body().lastName;
String stilling = response.body().stilling;
String mobil = response.body().mobiltelefon;
Log.d(TAG, "WordPress Login suksess! Rolle: " + role + ", UserID: " + userId);
// Lagre cookie, rolle og ID
UserManager.getInstance().setCookie(cookie);
UserManager.getInstance().setUserRole(role);
UserManager.getInstance().setUserId(userId);
// Lagre utvidet info i UserManager
UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil);
callback.onSuccess(role);
} else {
Log.e(TAG, "WordPress Login nektet. Kode: " + response.code());
callback.onError("Kunne ikke logge inn på Intranettet (Kode: " + response.code() + ")");
}
}
@Override
public void onFailure(Call call, Throwable t) {
Log.e(TAG, "Nettverksfeil mot WP", t);
callback.onError("Nettverksfeil: " + t.getMessage());
}
});
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarAdapter.java
============================================================
// FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarAdapter.java
package com.kbs.kbsintranett;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class CalendarAdapter extends RecyclerView.Adapter { // [cite: 31]
private List events;
public CalendarAdapter(List events) { // [cite: 32]
this.events = events;
} // [cite: 33]
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar, parent, false);
return new ViewHolder(view); // [cite: 34]
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CalendarEvent event = events.get(position);
holder.day.setText(event.getDay()); // [cite: 35]
holder.month.setText(event.getMonth());
// NYTT: Tidspunktet hentes nå fra getTime() som formateres i HomeFragment.
holder.time.setText(event.getTime());
holder.title.setText(event.getTitle());
}
@Override
public int getItemCount() {
return events.size(); // [cite: 36]
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView day, month, title, time; // NYTT: Lagt til time
public ViewHolder(View view) { // [cite: 37]
super(view);
day = view.findViewById(R.id.cal_day);
month = view.findViewById(R.id.cal_month); // [cite: 38]
title = view.findViewById(R.id.cal_title);
time = view.findViewById(R.id.cal_time); // NYTT
}
}
}
============================================================
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> {
@Override
public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Hvis feltet er "null" eller en tom tekststreng, returner en tom liste
if (json.isJsonNull() || (json.isJsonPrimitive() && json.getAsString().isEmpty())) {
return new ArrayList<>();
}
// Hvis det faktisk er en liste (Array), les den som vanlig
if (json.isJsonArray()) {
List list = new ArrayList<>();
for (JsonElement e : json.getAsJsonArray()) {
list.add(context.deserialize(e, GravityField.Choice.class));
}
return list;
}
// Hvis vi får noe annet rart (f.eks. en tekst som ikke er tom), ignorer det for å unngå krasj
return new ArrayList<>();
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\ConditionalLogicAdapter.java
============================================================
package com.kbs.kbsintranett;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
public class ConditionalLogicAdapter implements JsonDeserializer {
@Override
public GravityField.ConditionalLogic deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Hvis feltet er en streng (f.eks tom streng ""), returner null
if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) {
return null;
}
// Hvis det er et objekt, bruk standard deserialisering
if (json.isJsonObject()) {
// Vi må manuelt deserialisere for å unngå uendelig løkke hvis vi bare kaller context.deserialize på samme type
// Enkleste måte er å la Gson gjøre jobben på innholdet
return new com.google.gson.Gson().fromJson(json, GravityField.ConditionalLogic.class);
}
return null;
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsFragment.java
============================================================
package com.kbs.kbsintranett;
import android.Manifest;
import android.animation.LayoutTransition;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
public class FormsFragment extends Fragment {
private static final String TAG = "FormsFragment";
private static final String BASE_URL_GF = "https://intranet.kbs.no/wp-json/gf/v2";
// SKJEMA ID-er
private static final int ID_ANSATTEOPPLYSNINGER = 1;
private static final int ID_RUH = 4;
private static final int ID_SIKKERHETSKURS = 9;
private static final int ID_HMS_BEKREFTELSE = 10;
private static final int ID_EGENMELDING = 11;
private static final int ID_SJA = 14;
private static final int ID_FRAVARSVARSEL = 15;
private static final int ID_REFUSJON_UTLEGG = 16;
private int formId = 1;
private LinearLayout formContainer;
private LinearLayout historyContainer;
private View historyWrapper; // Wrapper for historikk-modulen
private TextView txtStatus;
private TextView lblHistory;
private ProgressBar loadingSpinner;
private ImageView btnToggleHistory;
// --- HOVEDSKJEMA STATE ---
private Map fieldWrappers = new HashMap<>();
private Map inputViews = new HashMap<>();
private Map requiredFieldsMap = new HashMap<>();
private Map fileUploads = new HashMap<>();
// --- NESTED FORM (BARN) STATE ---
private Map childInputViews = new HashMap<>();
private Map childRequiredFieldsMap = new HashMap<>();
private Map childFileUploads = new HashMap<>();
// Lagring av Nested Entries
private List nestedEntries = new ArrayList<>();
private LinearLayout nestedEntriesContainer;
private TextView totalAmountView;
// --- FILOPPLASTING & KAMERA ---
private String pendingFileFieldId = null;
private boolean isSelectingForChild = false;
private Uri currentPhotoUri = null;
private ActivityResultLauncher filePickerLauncher;
private ActivityResultLauncher takePictureLauncher;
private ActivityResultLauncher requestPermissionLauncher;
private GravityForm currentForm;
private final OkHttpClient client = new OkHttpClient();
private static final Pattern TITLE_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)");
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, uri, isSelectingForChild);
}
}
}
);
takePictureLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success && currentPhotoUri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild);
} else if (!success) {
currentPhotoUri = null;
}
}
);
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
openCamera();
} else {
Toast.makeText(getContext(), "Kameratillatelse er påkrevd for å ta bilde.", Toast.LENGTH_LONG).show();
}
}
);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_forms, container, false);
formContainer = view.findViewById(R.id.form_container);
historyContainer = view.findViewById(R.id.historyContainer);
historyWrapper = view.findViewById(R.id.history_wrapper);
txtStatus = view.findViewById(R.id.txt_status);
lblHistory = view.findViewById(R.id.lbl_history);
loadingSpinner = view.findViewById(R.id.loading_spinner);
btnToggleHistory = view.findViewById(R.id.btn_toggle_history);
if (btnToggleHistory != null) {
btnToggleHistory.setOnClickListener(v -> toggleHistoryVisibility());
}
// --- FIKS FOR NULLPOINTER EXCEPTION PÅ LAYOUTTRANSITION ---
if (view instanceof ViewGroup) {
LayoutTransition transition = ((ViewGroup) view).getLayoutTransition();
if (transition == null) {
transition = new LayoutTransition();
((ViewGroup) view).setLayoutTransition(transition);
}
transition.enableTransitionType(LayoutTransition.CHANGING);
}
// ----------------------------------------------------------
if (formContainer == null) {
formContainer = new LinearLayout(getContext());
}
if (getArguments() != null) {
int argId = getArguments().getInt("formId", 0);
if (argId != 0) formId = argId;
}
fetchFormStructure();
return view;
}
// --- UI LOGIKK FOR DELT SKJERM ---
private void expandFormModule() {
if (historyWrapper != null && historyWrapper.getVisibility() == View.VISIBLE) {
historyWrapper.setVisibility(View.GONE);
if (btnToggleHistory != null) {
btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float);
}
}
}
private void toggleHistoryVisibility() {
if (historyWrapper == null || btnToggleHistory == null) return;
if (historyWrapper.getVisibility() == View.VISIBLE) {
// Skjul historikk
historyWrapper.setVisibility(View.GONE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float);
} else {
// Vis historikk
historyWrapper.setVisibility(View.VISIBLE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float);
}
}
private void attachInteractionListener(View view) {
if (view == null) return;
view.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false;
});
view.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
expandFormModule();
}
});
if (view.isClickable()) {
view.setOnClickListener(v -> expandFormModule());
}
}
// ----------------------------------
private void fetchFormStructure() {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE);
updateStatus("Laster skjema...");
WordPressApiService api = RetrofitClient.getApiService();
api.getForm(formId).enqueue(new retrofit2.Callback() {
@Override
public void onResponse(retrofit2.Call call, retrofit2.Response response) {
if (response.isSuccessful() && response.body() != null) {
currentForm = response.body();
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
renderDynamicForm(currentForm);
fetchFormEntries();
});
}
} else {
updateStatus("Feil ved lasting av skjema.");
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
}
}
@Override
public void onFailure(retrofit2.Call call, Throwable t) {
updateStatus("Nettverksfeil (Skjema): " + t.getMessage());
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
}
});
}
private void renderDynamicForm(GravityForm form) {
if (formContainer == null) return;
formContainer.removeAllViews();
fieldWrappers.clear();
inputViews.clear();
requiredFieldsMap.clear();
fileUploads.clear();
nestedEntries.clear();
updateStatus("");
// Reset visibility of history on new load
if (historyWrapper != null) {
historyWrapper.setVisibility(View.VISIBLE);
if (btnToggleHistory != null) btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float);
}
TextView title = new TextView(getContext());
title.setText(getCleanTitle(form.title));
title.setTextSize(24);
title.setTypeface(null, Typeface.BOLD);
title.setTextColor(Color.BLACK);
title.setPadding(0, 0, 0, 20);
formContainer.addView(title);
if (form.description != null && !form.description.isEmpty()) {
TextView formDesc = new TextView(getContext());
String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", "");
formDesc.setText(cleanDesc);
formDesc.setPadding(0, 0, 0, 40);
formContainer.addView(formDesc);
}
if (form.fields == null) return;
for (GravityField field : form.fields) {
if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) {
continue;
}
LinearLayout fieldWrapper = new LinearLayout(getContext());
fieldWrapper.setOrientation(LinearLayout.VERTICAL);
fieldWrapper.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
fieldWrapper.setPadding(0, 10, 0, 20);
fieldWrappers.put(String.valueOf(field.id), fieldWrapper);
if ("section".equals(field.type)) {
addSectionHeader(fieldWrapper, field.label, field.description);
formContainer.addView(fieldWrapper);
continue;
}
if ("html".equals(field.type)) {
if (field.content != null && !field.content.isEmpty()) {
TextView htmlView = new TextView(getContext());
htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT));
fieldWrapper.addView(htmlView);
}
formContainer.addView(fieldWrapper);
continue;
}
TextView label = new TextView(getContext());
String labelText = field.label;
if (field.isRequired) labelText += " *";
label.setText(labelText);
label.setTextColor(Color.DKGRAY);
label.setTypeface(null, Typeface.BOLD);
label.setPadding(0, 10, 0, 5);
fieldWrapper.addView(label);
if ("form".equals(field.type)) {
renderNestedFormField(fieldWrapper, field);
}
else if ("product".equals(field.type) && "calculation".equals(field.inputType)) {
renderTotalSumField(fieldWrapper, field);
}
else if ("time".equals(field.type)) {
renderTimeField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("fileupload".equals(field.type)) {
renderFileUploadField(fieldWrapper, field, inputViews, requiredFieldsMap, false);
} else if (field.inputs != null && !field.inputs.isEmpty()) {
if ("consent".equals(field.type)) {
renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("checkbox".equals(field.type) || "multi_choice".equals(field.type)) {
renderCheckboxField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else {
renderCompositeField(fieldWrapper, field, inputViews, requiredFieldsMap);
}
} else if ("radio".equals(field.type)) {
renderRadioField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("select".equals(field.type)) {
renderSelectField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("textarea".equals(field.type)) {
renderTextAreaField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("date".equals(field.type)) {
renderDateField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("consent".equals(field.type)) {
renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else {
renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap);
}
if (field.description != null && !field.description.isEmpty()) {
TextView desc = new TextView(getContext());
desc.setText(Html.fromHtml(field.description, Html.FROM_HTML_MODE_COMPACT));
desc.setTextSize(12);
desc.setTextColor(Color.GRAY);
fieldWrapper.addView(desc);
}
formContainer.addView(fieldWrapper);
}
Button dynamicSubmit = new Button(getContext());
dynamicSubmit.setText("Send inn skjema");
dynamicSubmit.setTextColor(Color.WHITE);
dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 60, 0, 20);
dynamicSubmit.setLayoutParams(params);
dynamicSubmit.setOnClickListener(v -> submitDynamicForm());
formContainer.addView(dynamicSubmit);
evaluateAllConditionalLogic();
}
// --- NESTED FORM LOGIKK ---
private void renderNestedFormField(LinearLayout container, GravityField field) {
nestedEntriesContainer = new LinearLayout(getContext());
nestedEntriesContainer.setOrientation(LinearLayout.VERTICAL);
nestedEntriesContainer.setPadding(0, 10, 0, 10);
container.addView(nestedEntriesContainer);
Button btnAdd = new Button(getContext());
btnAdd.setText("Legg til vedlegg");
btnAdd.setBackgroundColor(Color.parseColor("#53AFE9"));
btnAdd.setTextColor(Color.WHITE);
btnAdd.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
int childFormId = 18;
if (field.gpnfForm != null) {
try {
childFormId = Integer.parseInt(field.gpnfForm);
} catch (NumberFormatException e) { e.printStackTrace(); }
}
// NYTT: Sender med felt-ID fra hovedskjemaet (f.eks "25")
openChildFormDialog(childFormId, field.id);
});
container.addView(btnAdd);
EditText hiddenIds = new EditText(getContext());
hiddenIds.setVisibility(View.GONE);
inputViews.put(field.id, hiddenIds);
}
private void renderTotalSumField(LinearLayout container, GravityField field) {
totalAmountView = new TextView(getContext());
totalAmountView.setText("Kr 0,00");
totalAmountView.setTextSize(18);
totalAmountView.setTypeface(null, Typeface.BOLD);
totalAmountView.setPadding(10, 10, 10, 10);
container.addView(totalAmountView);
}
// NY SIGNATUR: Tar imot parentFieldId
private void openChildFormDialog(int childFormId, String parentFieldId) {
if (getActivity() == null) return;
ProgressBar pBar = new ProgressBar(getContext());
AlertDialog loadingDialog = new AlertDialog.Builder(getContext())
.setView(pBar)
.setMessage("Laster skjema...")
.setCancelable(false)
.show();
RetrofitClient.getApiService().getForm(childFormId).enqueue(new retrofit2.Callback() {
@Override
public void onResponse(retrofit2.Call call, retrofit2.Response response) {
loadingDialog.dismiss();
if (response.isSuccessful() && response.body() != null) {
showChildFormDialog(response.body(), parentFieldId);
} else {
Toast.makeText(getContext(), "Kunne ikke hente underskjema", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call call, Throwable t) {
loadingDialog.dismiss();
Toast.makeText(getContext(), "Feil: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
// NY SIGNATUR: Tar imot parentFieldId
private void showChildFormDialog(GravityForm childForm, String parentFieldId) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
childInputViews.clear();
childRequiredFieldsMap.clear();
childFileUploads.clear();
ScrollView scrollView = new ScrollView(getContext());
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(30, 30, 30, 30);
scrollView.addView(layout);
for (GravityField field : childForm.fields) {
if ("hidden".equals(field.type) || field.isHidden) continue;
LinearLayout wrapper = new LinearLayout(getContext());
wrapper.setOrientation(LinearLayout.VERTICAL);
wrapper.setPadding(0, 10, 0, 20);
TextView label = new TextView(getContext());
String lText = field.label;
if (field.isRequired) lText += " *";
label.setText(lText);
label.setTypeface(null, Typeface.BOLD);
wrapper.addView(label);
if ("fileupload".equals(field.type)) {
renderFileUploadField(wrapper, field, childInputViews, childRequiredFieldsMap, true);
} else if ("product".equals(field.type)) {
EditText input = new EditText(getContext());
input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
input.setHint("Kr 0.00");
wrapper.addView(input);
childInputViews.put(field.id, input);
childRequiredFieldsMap.put(field.id, field.isRequired);
} else {
renderTextField(wrapper, field, childInputViews, childRequiredFieldsMap);
}
layout.addView(wrapper);
}
builder.setView(scrollView);
builder.setPositiveButton("Legg til vedlegg", null);
builder.setNegativeButton("Avbryt", (d, w) -> d.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
submitChildForm(childForm.id, dialog, parentFieldId);
});
}
// NY SIGNATUR: Tar imot parentFieldId og sender den som meta
private void submitChildForm(int childFormId, AlertDialog dialog, String parentFieldId) {
JSONObject inputValues = new JSONObject();
for (Map.Entry entry : childInputViews.entrySet()) {
String val = getInputValueGeneric(entry.getValue());
if (!val.isEmpty()) {
try {
inputValues.put("input_" + entry.getKey(), val);
} catch (JSONException e) { e.printStackTrace(); }
}
}
if (!childFileUploads.isEmpty()) {
List fileParts = new ArrayList<>();
Map textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String key = names.getString(i);
String val = inputValues.getString(key);
textParts.put(key, RequestBody.create(MultipartBody.FORM, val));
}
}
// --- HER ER FIKSEN FOR BUGGEN ---
// Vi legger ved en ekstra parameter som forteller Gravity Forms at dette
// er en nested entry som hører til et bestemt felt (f.eks "25").
if (parentFieldId != null) {
textParts.put("gpnf_entry_nested_form_field", RequestBody.create(MultipartBody.FORM, parentFieldId));
}
// ---------------------------------
for (Map.Entry fileEntry : childFileUploads.entrySet()) {
String fieldId = fileEntry.getKey();
Uri uri = fileEntry.getValue();
if (uri != null) {
MultipartBody.Part part = getFilePart("input_" + fieldId, uri);
if (part != null) fileParts.add(part);
}
}
Toast.makeText(getContext(), "Laster opp vedlegg...", Toast.LENGTH_SHORT).show();
RetrofitClient.getApiService().submitMultipartForm(childFormId, textParts, fileParts).enqueue(new retrofit2.Callback() {
@Override
public void onResponse(retrofit2.Call call, retrofit2.Response response) {
if (response.isSuccessful() && response.body() != null) {
try {
JsonObject json = response.body().getAsJsonObject();
if (json.has("is_valid") && json.get("is_valid").getAsBoolean()) {
String entryId = json.has("entry_id") ? json.get("entry_id").getAsString() : "";
// NB: Tilpass ID-ene her hvis skjema 18 endres.
// ID 3 = Beskrivelse, ID 4 = Beløp
String desc = getInputValueGeneric(childInputViews.get("3"));
String price = getInputValueGeneric(childInputViews.get("4"));
addNestedEntry(entryId, desc, price);
dialog.dismiss();
} else {
Toast.makeText(getContext(), "Ugyldig respons fra server", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(getContext(), "Feil ved parsing av svar", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getContext(), "Feil ved opplasting", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call call, Throwable t) {
Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) { e.printStackTrace(); }
}
}
private void addNestedEntry(String entryId, String description, String price) {
nestedEntries.add(new NestedEntry(entryId, description, price));
refreshNestedList();
}
private void refreshNestedList() {
if (nestedEntriesContainer == null) return;
nestedEntriesContainer.removeAllViews();
double total = 0;
List ids = new ArrayList<>();
for (NestedEntry entry : nestedEntries) {
ids.add(entry.id);
String cleanPrice = entry.price.replaceAll("[^0-9,.]", "").replace(",", ".");
try {
if (!cleanPrice.isEmpty()) total += Double.parseDouble(cleanPrice);
} catch (NumberFormatException e) { }
LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL);
row.setPadding(10, 10, 10, 10);
TextView txt = new TextView(getContext());
txt.setText(entry.description + " (" + entry.price + ")");
txt.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
row.addView(txt);
nestedEntriesContainer.addView(row);
}
if (totalAmountView != null) {
totalAmountView.setText("Totalt: Kr " + String.format("%.2f", total));
}
View hiddenField = inputViews.get("25");
if (hiddenField instanceof EditText) {
((EditText)hiddenField).setText(TextUtils.join(",", ids));
}
}
// --- FELLES METODER (FILE UPLOAD M/ CAMERA STØTTE) ---
private void renderFileUploadField(LinearLayout container, GravityField field, Map viewsMap, Map reqMap, boolean isChild) {
LinearLayout fileLayout = new LinearLayout(getContext());
fileLayout.setOrientation(LinearLayout.HORIZONTAL);
Button btnUpload = new Button(getContext());
btnUpload.setText("Velg fil / Ta bilde");
btnUpload.setOnClickListener(v -> {
// Setter state før vi viser dialog
pendingFileFieldId = field.id;
isSelectingForChild = isChild;
expandFormModule(); // Trigger expand
showFileSourceDialog();
});
TextView txtFileName = new TextView(getContext());
txtFileName.setText("Ingen fil valgt");
txtFileName.setPadding(20, 0, 0, 0);
txtFileName.setTextColor(Color.GRAY);
btnUpload.setTag(txtFileName);
fileLayout.addView(btnUpload);
fileLayout.addView(txtFileName);
container.addView(fileLayout);
viewsMap.put(field.id, btnUpload);
reqMap.put(field.id, field.isRequired);
}
// Hjelpemetode for å vise dialog
private void showFileSourceDialog() {
String[] options = {"Ta bilde", "Velg fil"};
new AlertDialog.Builder(getContext())
.setTitle("Last opp vedlegg")
.setItems(options, (dialog, which) -> {
if (which == 0) {
// Ta bilde - SJEKKER PERMISSION FØRST
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
// Spør om lov
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
}
} else {
// Velg fil
if (filePickerLauncher != null) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
String[] mimeTypes = {"image/jpeg", "image/png", "application/pdf"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
filePickerLauncher.launch(intent);
}
}
})
.show();
}
private void openCamera() {
currentPhotoUri = createImageUri();
if (currentPhotoUri != null && takePictureLauncher != null) {
try {
takePictureLauncher.launch(currentPhotoUri);
} catch (Exception e) {
Toast.makeText(getContext(), "Kunne ikke starte kamera: " + e.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Camera launch failed", e);
}
} else {
Toast.makeText(getContext(), "Kunne ikke opprette bildefil", Toast.LENGTH_SHORT).show();
}
}
private Uri createImageUri() {
try {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private void handleFileSelection(String fieldId, Uri uri, boolean isChild) {
if (isChild) {
childFileUploads.put(fieldId, uri);
} else {
fileUploads.put(fieldId, uri);
}
Map targetMap = isChild ? childInputViews : inputViews;
View view = targetMap.get(fieldId);
if (view instanceof Button) {
TextView txtView = (TextView) view.getTag();
if (txtView != null) {
txtView.setText(getFileName(uri));
txtView.setTextColor(Color.BLACK);
}
}
}
private String getFileName(Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if(index >= 0) result = cursor.getString(index);
}
} catch (Exception e) {}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) result = result.substring(cut + 1);
}
return result;
}
private String getCleanTitle(String title) {
if (title == null) return "";
Matcher m = TITLE_PATTERN.matcher(title.trim());
if (m.find()) {
return m.group(2);
}
return title;
}
// --- STANDARD RENDER METODER ---
private void renderTimeField(LinearLayout container, GravityField field, Map views, Map req) {
EditText timeInput = new EditText(getContext());
timeInput.setFocusable(false);
timeInput.setClickable(true);
timeInput.setHint("00:00");
timeInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar mcurrentTime = Calendar.getInstance();
int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY);
int minute = mcurrentTime.get(Calendar.MINUTE);
new TimePickerDialog(getContext(), (timePicker, selectedHour, selectedMinute) -> {
timeInput.setText(String.format("%02d:%02d", selectedHour, selectedMinute));
evaluateAllConditionalLogic();
}, hour, minute, true).show();
});
container.addView(timeInput);
views.put(field.id, timeInput);
req.put(field.id, field.isRequired);
}
private void renderTextField(LinearLayout container, GravityField field, Map views, Map req) {
EditText input = new EditText(getContext());
input.setPadding(30, 30, 30, 30);
input.setBackgroundResource(android.R.drawable.edit_text);
if ("number".equals(field.type) || "phone".equals(field.type)) {
input.setInputType(InputType.TYPE_CLASS_PHONE);
} else if ("email".equals(field.type)) {
input.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
} else {
input.setInputType(InputType.TYPE_CLASS_TEXT);
}
if (views == inputViews) {
UserManager user = UserManager.getInstance();
String lowerLabel = field.label.toLowerCase();
if (lowerLabel.contains("e-post")) input.setText(user.getUserEmail());
if (lowerLabel.contains("navn") || lowerLabel.contains("melder")) input.setText(user.getUserDisplayName());
if (lowerLabel.contains("stilling")) input.setText(user.getStilling());
if (lowerLabel.contains("mobil")) input.setText(user.getMobiltelefon());
}
input.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); }
});
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderTextAreaField(LinearLayout container, GravityField field, Map views, Map req) {
EditText input = new EditText(getContext());
input.setBackgroundResource(android.R.drawable.edit_text);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
input.setMinLines(3);
input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START);
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderRadioField(LinearLayout container, GravityField field, Map views, Map req) {
RadioGroup group = new RadioGroup(getContext());
if (field.choices != null) {
for (GravityField.Choice choice : field.choices) {
RadioButton rb = new RadioButton(getContext());
rb.setText(choice.text);
rb.setTag(choice.value);
// Also trigger expand on RadioButton click
rb.setOnClickListener(v -> {
expandFormModule();
evaluateAllConditionalLogic();
});
group.addView(rb);
}
}
// Fallback listener
group.setOnCheckedChangeListener((g, i) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(group);
views.put(field.id, group);
req.put(field.id, field.isRequired);
}
private void renderSelectField(LinearLayout container, GravityField field, Map views, Map req) {
Spinner spinner = new Spinner(getContext());
// Spinner touch listener is tricky, usually set onTouchListener works
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false;
});
List labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) {
for (GravityField.Choice c : field.choices) labels.add(c.text);
}
ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, labels);
spinner.setAdapter(adapter);
container.addView(spinner);
views.put(field.id, spinner);
req.put(field.id, field.isRequired);
}
private void renderConsentField(LinearLayout container, GravityField field, Map views, Map req) {
CheckBox checkBox = new CheckBox(getContext());
String cbText = (field.checkboxLabel != null && !field.checkboxLabel.isEmpty()) ? field.checkboxLabel : field.label;
checkBox.setText(cbText);
String inputId = (field.inputs != null && !field.inputs.isEmpty()) ? field.inputs.get(0).id : field.id;
checkBox.setTag("1");
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputId, checkBox);
req.put(inputId, field.isRequired);
}
private void renderCheckboxField(LinearLayout container, GravityField field, Map views, Map req) {
if (field.inputs != null) {
for (int i = 0; i < field.inputs.size(); i++) {
GravityField inputDef = field.inputs.get(i);
CheckBox checkBox = new CheckBox(getContext());
checkBox.setText(inputDef.label);
String value = "1";
// Fallback
if (field.choices != null && i < field.choices.size()) {
value = field.choices.get(i).value;
}
checkBox.setTag(value);
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputDef.id, checkBox);
req.put(inputDef.id, false);
}
}
}
private void renderDateField(LinearLayout container, GravityField field, Map views, Map req) {
EditText dateInput = new EditText(getContext());
// --- DATO LØSNING (Fix for Read Only / Dagens Dato) ---
if (field.readOnly || (formId == ID_REFUSJON_UTLEGG && "28".equals(field.id))) {
// Sett dagens dato
SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault());
dateInput.setText(df.format(new Date()));
// Gjør den "read-only" men synlig
dateInput.setFocusable(false);
dateInput.setClickable(false);
dateInput.setEnabled(false);
dateInput.setTextColor(Color.BLACK);
} else {
// Vanlig dato-velger
dateInput.setFocusable(false);
dateInput.setClickable(true);
dateInput.setHint("dd.mm.yyyy");
dateInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar c = Calendar.getInstance();
new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> {
dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year));
evaluateAllConditionalLogic();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
});
}
dateInput.setPadding(30, 30, 30, 30);
dateInput.setBackgroundResource(android.R.drawable.edit_text);
container.addView(dateInput);
views.put(field.id, dateInput);
req.put(field.id, field.isRequired);
}
private void renderCompositeField(LinearLayout container, GravityField parentField, Map views, Map req) {
UserManager user = UserManager.getInstance();
boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER);
List inputs = new ArrayList<>(parentField.inputs);
if ("address".equals(parentField.type)) {
Collections.sort(inputs, (f1, f2) -> Integer.compare(getAddressScore(f1.label), getAddressScore(f2.label)));
}
for (GravityField subField : inputs) {
if (subField.isHidden || "hidden".equals(subField.visibility)) continue;
TextView subLabel = new TextView(getContext());
String subLabelText = subField.label;
boolean isSubRequired = parentField.isRequired;
if ("address".equals(parentField.type) && subField.id.endsWith(".2")) {
isSubRequired = false;
}
if (isSubRequired) subLabelText += " *";
subLabel.setText(subLabelText);
subLabel.setTextColor(Color.GRAY);
subLabel.setTextSize(12);
subLabel.setPadding(0, 10, 0, 0);
container.addView(subLabel);
EditText subInput = new EditText(getContext());
subInput.setPadding(30, 30, 30, 30);
subInput.setBackgroundResource(android.R.drawable.edit_text);
subInput.setInputType(InputType.TYPE_CLASS_TEXT);
if (isPersonalia && parentField.label.toLowerCase().contains("navn") && !parentField.label.toLowerCase().contains("pårørende")) {
String lowerSub = subField.label.toLowerCase();
if (lowerSub.contains("fornavn")) subInput.setText(user.getFirstName());
else if (lowerSub.contains("etternavn")) subInput.setText(user.getLastName());
}
attachInteractionListener(subInput);
container.addView(subInput);
views.put(subField.id, subInput);
req.put(subField.id, isSubRequired);
}
}
private void addSectionHeader(LinearLayout container, String title, String descText) {
TextView sectionHeader = new TextView(getContext());
sectionHeader.setText(title);
sectionHeader.setTextSize(18);
sectionHeader.setTypeface(null, Typeface.BOLD);
sectionHeader.setTextColor(Color.parseColor("#0069B3"));
sectionHeader.setPadding(0, 20, 0, 5);
container.addView(sectionHeader);
if (descText != null && !descText.isEmpty()) {
TextView desc = new TextView(getContext());
desc.setText(Html.fromHtml(descText, Html.FROM_HTML_MODE_COMPACT));
desc.setTextSize(12);
desc.setTextColor(Color.GRAY);
container.addView(desc);
}
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2));
line.setBackgroundColor(Color.LTGRAY);
line.setPadding(0,0,0,20);
container.addView(line);
}
private int getAddressScore(String label) {
if (label == null) return 99;
String l = label.toLowerCase();
if (l.contains("adresselinje 1")) return 1;
if (l.contains("adresselinje 2")) return 2;
if (l.contains("postnummer") || l.contains("zip")) return 3;
if (l.contains("poststed") || l.contains("city")) return 4;
if (l.contains("land") || l.contains("country")) return 5;
return 99;
}
private void evaluateAllConditionalLogic() {
if (currentForm == null || currentForm.fields == null) return;
for (GravityField field : currentForm.fields) {
if (field.conditionalLogic == null) {
setViewVisibility(field.id, true);
continue;
}
boolean isMatch = evaluateLogic(field.conditionalLogic);
boolean show = "show".equalsIgnoreCase(field.conditionalLogic.actionType);
boolean shouldBeVisible = (show && isMatch) || (!show && !isMatch);
setViewVisibility(field.id, shouldBeVisible);
}
}
private boolean evaluateLogic(GravityField.ConditionalLogic logic) {
if (logic.rules == null || logic.rules.isEmpty()) return true;
boolean isAll = "all".equalsIgnoreCase(logic.logicType);
boolean aggregatedResult = isAll;
for (GravityField.Rule rule : logic.rules) {
String val = getInputValue(rule.fieldId);
boolean ruleMatch = checkRule(val, rule.operator, rule.value);
if (isAll) {
aggregatedResult = aggregatedResult && ruleMatch;
if (!aggregatedResult) break;
} else {
aggregatedResult = aggregatedResult ||
ruleMatch;
if (aggregatedResult) break;
}
}
return aggregatedResult;
}
private boolean checkRule(String actualValue, String operator, String targetValue) {
if (actualValue == null) actualValue = "";
if (targetValue == null) targetValue = "";
switch (operator.toLowerCase()) {
case "is": return actualValue.equalsIgnoreCase(targetValue);
case "isnot": return !actualValue.equalsIgnoreCase(targetValue);
case "contains": return actualValue.toLowerCase().contains(targetValue.toLowerCase());
case "starts_with": return actualValue.toLowerCase().startsWith(targetValue.toLowerCase());
case "ends_with": return actualValue.toLowerCase().endsWith(targetValue.toLowerCase());
default: return false;
}
}
private String getInputValue(String fieldId) {
View view = inputViews.get(fieldId);
return getInputValueGeneric(view);
}
private String getInputValueGeneric(View view) {
if (view == null) return "";
if (view instanceof EditText) return ((EditText) view).getText().toString();
if (view instanceof RadioGroup) {
int id = ((RadioGroup) view).getCheckedRadioButtonId();
if (id != -1) {
View rb = view.findViewById(id);
if (rb != null && rb.getTag() != null) return rb.getTag().toString();
}
}
if (view instanceof Spinner) {
if (((Spinner) view).getSelectedItemPosition() == 0) return "";
Object item = ((Spinner) view).getSelectedItem();
return item != null ? item.toString() : "";
}
if (view instanceof CheckBox) {
CheckBox cb = (CheckBox) view;
if (cb.isChecked()) {
return cb.getTag() != null ?
cb.getTag().toString() : "1";
}
return "";
}
return "";
}
private void setViewVisibility(String fieldId, boolean visible) {
View wrapper = fieldWrappers.get(fieldId);
if (wrapper != null) {
wrapper.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
// --- SUBMISSION ---
private void submitDynamicForm() {
JSONObject inputValues = new JSONObject();
boolean hasValues = false;
Log.d(TAG, "submitDynamicForm: Starting validation...");
for (Map.Entry entry : inputViews.entrySet()) {
String fieldId = entry.getKey();
View view = entry.getValue();
View wrapper = fieldWrappers.get(fieldId);
if (wrapper == null) {
if (!view.isShown()) continue;
} else {
if (wrapper.getVisibility() != View.VISIBLE) continue;
}
String val = getInputValueGeneric(view);
Boolean req = requiredFieldsMap.get(fieldId);
if (req != null && req && val.isEmpty() && !(view instanceof Button)) {
Log.d(TAG, "Validation failed for field " + fieldId);
if (view instanceof EditText) {
((EditText)view).setError("Må fylles ut");
view.requestFocus();
} else {
Toast.makeText(getContext(), "Fyll ut alle felt", Toast.LENGTH_SHORT).show();
}
return;
}
if (!val.isEmpty()) {
try {
GravityField fieldDef = getGravityFieldById(fieldId);
if (fieldDef != null && "date".equals(fieldDef.type)) {
val = formatDateForApi(val);
}
inputValues.put("input_" + fieldId, val);
hasValues = true;
} catch (JSONException e) {}
}
}
if (!hasValues && fileUploads.isEmpty()) {
Log.d(TAG, "Submit aborted: Form is empty");
Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show();
return;
}
updateStatus("Sender inn...");
String cookie = UserManager.getInstance().getCookie();
Log.d(TAG, "Preparing submission payload: " + inputValues.toString());
if (!fileUploads.isEmpty()) {
Log.d(TAG, "Submitting as Multipart...");
sendMultipart(inputValues);
} else {
Log.d(TAG, "Submitting as JSON...");
RequestBody body = RequestBody.create(MediaType.parse("application/json"), inputValues.toString());
String url = BASE_URL_GF + "/forms/" + formId + "/submissions";
Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build();
client.newCall(request).enqueue(new okhttp3.Callback() {
public void onFailure(okhttp3.Call call, IOException e) {
Log.e(TAG, "JSON submit failed", e);
updateStatus("Feil: " + e.getMessage());
}
public void onResponse(okhttp3.Call call, Response response) {
Log.d(TAG, "JSON response code: " + response.code());
if (response.isSuccessful()) {
if (getActivity() != null) getActivity().runOnUiThread(() -> {
Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show();
fetchFormEntries();
updateStatus("OK");
clearInputs();
});
} else {
try {
String errBody = response.body() != null ? response.body().string() : "No body";
Log.e(TAG, "Server error body: " + errBody);
updateStatus("Feil (" + response.code() + "): " + errBody);
} catch(Exception e){}
}
}
});
}
}
private String formatDateForApi(String dateStr) {
if (dateStr == null || dateStr.isEmpty()) return "";
try {
SimpleDateFormat displayFormat = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault());
Date date = displayFormat.parse(dateStr);
SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
return apiFormat.format(date);
} catch (Exception e) {
return dateStr;
}
}
private GravityField getGravityFieldById(String id) {
if (currentForm == null || currentForm.fields == null) return null;
for (GravityField f : currentForm.fields) {
if (f.id.equals(id)) return f;
if (f.inputs != null) {
for (GravityField sub : f.inputs) {
if (sub.id.equals(id)) return sub;
}
}
}
return null;
}
private void sendMultipart(JSONObject inputValues) {
List fileParts = new ArrayList<>();
Map textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for(int i=0; i entry : fileUploads.entrySet()) {
MultipartBody.Part part = getFilePart("input_" + entry.getKey(), entry.getValue());
if (part != null) fileParts.add(part);
}
RetrofitClient.getApiService().submitMultipartForm(formId, textParts, fileParts).enqueue(new retrofit2.Callback() {
public void onResponse(retrofit2.Call call, retrofit2.Response response) {
if (response.isSuccessful()) {
if (getActivity() != null) getActivity().runOnUiThread(() -> {
Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show();
fetchFormEntries();
updateStatus("OK");
clearInputs();
});
} else {
updateStatus("Feil: " + response.code());
}
}
public void onFailure(retrofit2.Call call, Throwable t) { updateStatus("Feil: " + t.getMessage()); }
});
} catch (Exception e) {}
}
private MultipartBody.Part getFilePart(String partName, Uri uri) {
try {
InputStream inputStream = getContext().getContentResolver().openInputStream(uri);
String fileName = getFileName(uri);
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() { return MediaType.parse("application/octet-stream");
}
@Override public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(inputStream)) { sink.writeAll(source);
}
}
};
return MultipartBody.Part.createFormData(partName, fileName, requestBody);
} catch (Exception e) { return null;
}
}
private void fetchFormEntries() {
UserManager user = UserManager.getInstance();
String cookie = user.getCookie();
int userId = user.getUserId();
if (cookie == null) return;
String searchJson = "{\"field_filters\":[{\"key\":\"created_by\",\"value\":\"" + userId + "\"}]}";
String encodedSearch = "";
try {
encodedSearch = URLEncoder.encode(searchJson, "UTF-8");
} catch (UnsupportedEncodingException e) { e.printStackTrace(); }
String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch;
Request request = new Request.Builder().url(url).header("Cookie", cookie).build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException e) {
Log.e(TAG, "Kunne ikke hente historikk", e);
}
@Override
public void onResponse(@NonNull okhttp3.Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
String jsonStr = response.body().string();
try {
JSONObject json = new JSONObject(jsonStr);
if (json.has("entries")) {
JSONArray entries = json.getJSONArray("entries");
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
showHistory(entries);
if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0)
{
try {
prefillFormFromHistory(entries.getJSONObject(0));
} catch (JSONException e) { e.printStackTrace(); }
}
});
}
}
} catch (JSONException e) { e.printStackTrace();
}
}
}
});
}
private void showHistory(JSONArray entries) {
if (historyContainer == null) return;
historyContainer.removeAllViews();
if (entries.length() == 0) {
if (lblHistory != null) lblHistory.setVisibility(View.GONE);
return;
} else {
if (lblHistory != null) lblHistory.setVisibility(View.VISIBLE);
}
try {
// Vis flere oppføringer siden vi nå har scrolle-mulighet øverst
int count = Math.min(entries.length(), 20);
for (int i = 0; i < count; i++) {
JSONObject entry = entries.getJSONObject(i);
String date = entry.optString("date_created");
// Prøv å finne en bedre tittel enn bare dato (f.eks Prosjektnavn eller Sted)
String titleText = "Innsendt: " + date;
TextView item = new TextView(getContext());
item.setText(titleText);
item.setPadding(10, 20, 10, 20);
item.setBackgroundResource(android.R.drawable.list_selector_background);
item.setTextSize(14);
// Add click listener to show details
item.setOnClickListener(v -> showEntryDetails(entry));
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
line.setBackgroundColor(Color.LTGRAY);
historyContainer.addView(item);
historyContainer.addView(line);
}
} catch (JSONException e) { e.printStackTrace();
}
}
// NY METODE: Vis detaljer i dialog med delingsknapp
private void showEntryDetails(JSONObject entry) {
// HVIS dette er Form 16 (Refusjon), må vi først hente vedleggene (som ligger i Form 18)
if (formId == ID_REFUSJON_UTLEGG) {
Log.d(TAG, "Form 16 detected. Checking for child entries...");
String nestedIds = entry.optString("25"); // Felt 25 i Form 16 inneholder ID-ene til vedleggene
if (!nestedIds.isEmpty()) {
Log.d(TAG, "Nested IDs found: " + nestedIds);
List ids = new ArrayList<>();
// ROBUST PARSING AV ID-LISTE
if (nestedIds.startsWith("[") && nestedIds.endsWith("]")) {
// Dette er en JSON-array streng (f.eks [101, 102])
try {
JSONArray jsonArray = new JSONArray(nestedIds);
for(int i=0; iInnsendt: ").append(date).append("
");
text.append("Innsendt: ").append(date).append("\n\n");
if (currentForm != null && currentForm.fields != null) {
for (GravityField field : currentForm.fields) {
if ("section".equals(field.type) || "html".equals(field.type) || "captcha".equals(field.type)) continue;
// Hopp over felt 25 (Vedleggs-IDer) i Form 16, siden vi viser det bedre senere
if (formId == ID_REFUSJON_UTLEGG && "25".equals(field.id)) continue;
String value = "";
if (field.inputs != null && !field.inputs.isEmpty()) {
for (GravityField input : field.inputs) {
String subVal = entry.optString(input.id);
if (!subVal.isEmpty()) value += " " + subVal;
}
} else {
value = entry.optString(String.valueOf(field.id));
}
if (!value.trim().isEmpty()) {
if ("fileupload".equals(field.type)) {
// Håndter filopplastinger (single/multiple)
// Her antar vi enkel URL, men for sikkerhets skyld bruker vi extractUrl
String cleanUrl = extractUrl(value);
if (cleanUrl.startsWith("http")) {
html.append("").append(field.label).append(": ")
.append("Åpne fil