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.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.Intent; import android.database.Cursor; import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; 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.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; 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.fragment.app.Fragment; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; 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.Call; 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 TextView txtStatus; private TextView lblHistory; private ProgressBar loadingSpinner; // --- 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; private String pendingFileFieldId = null; private boolean isSelectingForChild = false; private ActivityResultLauncher filePickerLauncher; 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); } } pendingFileFieldId = null; isSelectingForChild = false; } ); } @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() { @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(""); 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 -> { int childFormId = 18; if (field.gpnfForm != null) { try { childFormId = Integer.parseInt(field.gpnfForm); } catch (NumberFormatException e) { e.printStackTrace(); } } openChildFormDialog(childFormId); }); 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); } 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() { @Override public void onResponse(retrofit2.Call call, retrofit2.Response 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 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()); // FJERNET: builder.setTitle(childForm.title); - Brukeren ønsket ikke tittel i popup 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 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)); } } 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 --- 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"); 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); } 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 -> { 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(); } }); 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); 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); 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 views, Map req) { Spinner spinner = new Spinner(getContext()); 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; // For Consent fields, Gravity Forms typically expects "1" if checked. checkBox.setTag("1"); 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 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); // --- 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); // ------------------------------------------- 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 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 -> { 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()); } 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() : ""; } // --- 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 ""; } // ---------------------------------------- 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 { // --- 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); 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(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 { // --- BEDRE LOGGING FOR 400 FEIL --- String errBody = response.body() != null ? response.body().string() : "No body"; Log.e(TAG, "Server error body: " + errBody); updateStatus("Feil (" + response.code() + "): " + errBody); // Viser feilmeldingen i UI } catch(Exception e){} } } }); } } // 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 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 Call call, @NonNull IOException e) { Log.e(TAG, "Kunne ikke hente historikk", e); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if (response.isSuccessful()) { String jsonStr = response.body().string(); try { JSONObject json = new JSONObject(jsonStr); if (json.has("entries")) { JSONArray entries = json.getJSONArray("entries"); if (getActivity() != null) { getActivity().runOnUiThread(() -> { showHistory(entries); 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 { int count = Math.min(entries.length(), 5); for (int i = 0; i < count; i++) { JSONObject entry = entries.getJSONObject(i); String date = entry.optString("date_created"); TextView item = new TextView(getContext()); item.setText("Innsendt: " + date); 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); historyContainer.addView(item); historyContainer.addView(line); } } catch (JSONException e) { e.printStackTrace(); } } private void prefillFormFromHistory(JSONObject latestEntry) { if (latestEntry == null) return; for (Map.Entry entry : inputViews.entrySet()) { String fieldId = entry.getKey(); View view = entry.getValue(); if (latestEntry.has(fieldId)) { String value = latestEntry.optString(fieldId); if (value == null || value.isEmpty()) continue; if (view instanceof EditText) { ((EditText) view).setText(value); } else if (view instanceof RadioGroup) { RadioGroup group = (RadioGroup) view; for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); if (child instanceof RadioButton) { Object tag = child.getTag(); if (tag != null && tag.toString().equalsIgnoreCase(value)) { ((RadioButton) child).setChecked(true); break; } } } } else if (view instanceof CheckBox) { if ("1".equals(value) || "true".equalsIgnoreCase(value) || ((CheckBox)view).getText().toString().equals(value)) { ((CheckBox) view).setChecked(true); } } } } updateStatus("Skjemaet er forhåndsutfylt fra din siste innsending."); evaluateAllConditionalLogic(); } private void clearInputs() { for (View view : inputViews.values()) { if (view instanceof EditText) { ((EditText) view).setText(""); } else if (view instanceof CheckBox) { ((CheckBox) view).setChecked(false); } else if (view instanceof RadioGroup) { ((RadioGroup) view).clearCheck(); } else if (view instanceof Button) { // 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"); } } } 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; } } private void updateStatus(String msg) { if (getActivity() != null && txtStatus != null) { getActivity().runOnUiThread(() -> txtStatus.setText(msg)); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsListFragment.java ============================================================ package com.kbs.kbsintranett; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class FormsListFragment extends Fragment { private static final String TAG = "FormsListFragment"; private LinearLayout container; private ProgressBar loadingSpinner; private TextView txtStatus; // 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*(.*)"); @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); View historyTitle = view.findViewById(R.id.historyContainer); 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(""); // Vi bruker standard-kallet som er raskt og effektivt RetrofitClient.getApiService().getFormsListMap().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (getContext() == null) return; if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { Map formMap = response.body(); if (formMap.isEmpty()) { if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet."); return; } List 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; } // Sorter listen basert på tallet i tittelen Collections.sort(visibleForms, new Comparator() { @Override public int compare(GravityForm f1, GravityForm f2) { return Integer.compare(getSortOrderFromTitle(f1.title), getSortOrderFromTitle(f2.title)); } }); for (GravityForm form : visibleForms) { addFormButton(form); } } else { if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")"); } } @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage()); } }); } /** * 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) } private void addFormButton(GravityForm form) { Button button = new Button(getContext()); // VIS VASKET TITTEL PÅ KNAPPEN String displayTitle = getCleanTitle(form.title); button.setText(displayTitle.toUpperCase()); 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 ); params.setMargins(0, 0, 0, 20); 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 inputValues; public FormSubmission(Map inputValues) { this.inputValues = inputValues; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; import java.util.Map; public class GravityEntryResponse { @SerializedName("total_count") public int totalCount; @SerializedName("entries") public List> entries; // Vi bruker Map fordi Gravity Forms returnerer alle feltverdier som nøkkel/verdi par i roten av objektet. // F.eks: { "id": "100", "form_id": "1", "1.3": "Ola", "1.6": "Nordmann" } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityField.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import java.util.List; public class GravityField { @SerializedName("id") public String id; @SerializedName("type") public String type; @SerializedName("inputType") public String inputType; @SerializedName("label") public String label; @SerializedName("adminLabel") public String adminLabel; @SerializedName("description") public String description; @SerializedName("defaultValue") public String defaultValue; @SerializedName("isRequired") public boolean isRequired; @SerializedName("checkboxLabel") public String checkboxLabel; @SerializedName("visibility") public String visibility; @JsonAdapter(ChoicesAdapter.class) @SerializedName("choices") public List choices; @SerializedName("content") public String content; @SerializedName("inputs") public List inputs; @SerializedName("isHidden") public boolean isHidden; // NYTT: For å sjekke om feltet er Read Only (f.eks dato i refusjon) @SerializedName("gwreadonly_enable") public boolean readOnly; @JsonAdapter(ConditionalLogicAdapter.class) @SerializedName("conditionalLogic") public ConditionalLogic conditionalLogic; @SerializedName("gppa-values-templates") public java.util.Map gppaTemplates; @SerializedName("gpnfForm") public String gpnfForm; public static class Choice { @SerializedName("text") public String text; @SerializedName("value") public String value; } public static class ConditionalLogic { @SerializedName("actionType") public String actionType; @SerializedName("logicType") public String logicType; @SerializedName("rules") public List rules; } public static class Rule { @SerializedName("fieldId") public String fieldId; @SerializedName("operator") public String operator; @SerializedName("value") public String value; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityForm.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; public class GravityForm { @SerializedName("id") public int id; @SerializedName("title") public String title; @SerializedName("description") public String description; @SerializedName("is_active") public String isActive; // "1" = Aktiv, "0" = Inaktiv @SerializedName("fields") public List fields; } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookFragment.java ============================================================ package com.kbs.kbsintranett; import androidx.fragment.app.Fragment; // Viktig import! public class HandbookFragment extends Fragment { // Tomt innhold er OK, men klassen må hete det samme som filnavnet // og den må arve fra (extends) Fragment. public HandbookFragment() { super(R.layout.fragment_home); // Kobler til layouten automatisk } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java package com.kbs.kbsintranett; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class HomeFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // Laster inn layouten fra XML (fragment_home.xml) return inflater.inflate(R.layout.fragment_home, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // --------------------------------------------------------- // 0. SETT OPP PROFIL-KNAPP (Ny!) // --------------------------------------------------------- // Vi finner ikonet vi la til i XML og sier at det skal gå til Profil-siden View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { profileBtn.setOnClickListener(v -> { Navigation.findNavController(view).navigate(R.id.navigation_profile); }); } // --------------------------------------------------------- // 1. SETT OPP KALENDER (Henter fra WordPress) // --------------------------------------------------------- RecyclerView calendarRecycler = view.findViewById(R.id.recycler_calendar); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); // Starter henting av kalenderdata fetchCalendarEvents(calendarRecycler); // --------------------------------------------------------- // 2. SETT OPP NYHETER (Hentes fra WordPress) // --------------------------------------------------------- RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); // Gjør at scrollen flyter bedre inni NestedScrollView (hvis du bruker det i XML) newsRecycler.setNestedScrollingEnabled(false); // Start henting av ekte data fetchNewsFromWordpress(newsRecycler); } /** * Henter kalenderhendelser fra WordPress via RetrofitClient */ private void fetchCalendarEvents(RecyclerView recyclerView) { // 1. Hent API-tjenesten vår WordPressApiService apiService = RetrofitClient.getApiService(); // 2. Send forespørsel til nettet (Asynkront) apiService.getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (getContext() == null || response.body() == null) return; List rawEvents = response.body(); List formattedEvents = new ArrayList<>(); // Formater for parsing og visning SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone SimpleDateFormat dayFormat = new SimpleDateFormat("dd", Locale.getDefault()); dayFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone // Bruker norsk locale for måned (Jan, Feb, Mar, etc.) SimpleDateFormat monthFormat = new SimpleDateFormat("MMM", new Locale("no", "NO")); monthFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault()); timeFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone for (CalendarEvent event : rawEvents) { try { // Bruker getRawDate() fra CalendarEvent.java (oppdatert) Date date = apiFormat.parse(event.getRawDate()); String day = dayFormat.format(date); String month = monthFormat.format(date).toUpperCase(Locale.getDefault()); String startTime = timeFormat.format(date); // Bruker den gamle konstruktøren for å sette formaterte data i adapteren formattedEvents.add(new CalendarEvent( event.getTitle(), startTime, day, month )); } catch (ParseException e) { e.printStackTrace(); // Håndterer feil i parsing av dato/tid ved å vise rå data formattedEvents.add(new CalendarEvent(event.getTitle(), "Ukjent", event.getDay(), event.getMonth())); } } recyclerView.setAdapter(new CalendarAdapter(formattedEvents)); } @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; System.err.println("Kalender Nettverksfeil: " + t.getMessage()); // Vis feilmelding i RecyclerView List errorList = new ArrayList<>(); errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverket ditt.", "00", "FEIL")); recyclerView.setAdapter(new CalendarAdapter(errorList)); } }); } /** * Henter nyheter fra WordPress via RetrofitClient */ private void fetchNewsFromWordpress(RecyclerView recyclerView) { // 1. Hent API-tjenesten vår WordPressApiService apiService = RetrofitClient.getApiService(); // 2. Send forespørsel til nettet (Asynkront) apiService.getPosts().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { // Sjekk om appen fortsatt lever (viktig for å unngå krasj) if (getContext() == null) return; if (response.isSuccessful() && response.body() != null) { // 3. Suksess! Vi fikk data fra WordPress. List wpPosts = response.body(); List newsList = new ArrayList<>(); // Datoformatering for nyhetene SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone // Konverter fra "WpPost" (API-format) til "NewsItem" (App-format) for (WpPost post : wpPosts) { String formattedDate = post.date; try { // API-datoen (post.date) er i formatet "yyyy-MM-dd'T'HH:mm:ss" Date date = rawFormat.parse(post.date); formattedDate = targetFormat.format(date); } catch (ParseException e) { System.err.println("Feil ved parsing av nyhetsdato: " + e.getMessage()); } newsList.add(new NewsItem( post.getTitleStr(), post.getExcerptStr(), "Publisert: " + formattedDate )); } // 4. Send listen til Adapteren slik at den vises på skjermen NewsAdapter adapter = new NewsAdapter(newsList); recyclerView.setAdapter(adapter); } else { System.err.println("Feil: Fikk svar, men noe var galt med dataene: " + response.code()); // Her kunne vi vist en "Ingen nyheter"-tekst // (Løsningen har allerede lagt inn fallback i onFailure) } } @Override public void onFailure(Call> call, Throwable t) { // Nettverksfeil (Ingen nett, feil URL, etc) if (getContext() == null) return; System.err.println("Nettverksfeil: " + t.getMessage()); // Legg til en "Feilmelding" i listen så brukeren ser det List errorList = new ArrayList<>(); errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System")); recyclerView.setAdapter(new NewsAdapter(errorList)); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginFragment.java ============================================================ package com.kbs.kbsintranett; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; import androidx.navigation.Navigation; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.SignInButton; import com.google.android.gms.common.api.ApiException; import com.google.android.gms.tasks.Task; public class LoginFragment extends Fragment { private static final String TAG = "LoginFragment"; private GoogleSignInClient mGoogleSignInClient; private TextView statusText; private SignInButton signInButton; // Håndterer resultatet fra Google-vinduet private final ActivityResultLauncher signInLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { Task task = GoogleSignIn.getSignedInAccountFromIntent(result.getData()); handleGoogleResult(task); } ); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_login, container, false); statusText = view.findViewById(R.id.status_text); signInButton = view.findViewById(R.id.sign_in_button); signInButton.setSize(SignInButton.SIZE_WIDE); // Hent ID fra MainActivity String clientId = MainActivity.GOOGLE_WEB_CLIENT_ID; GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(clientId) .requestEmail() .build(); mGoogleSignInClient = GoogleSignIn.getClient(requireActivity(), gso); signInButton.setOnClickListener(v -> { statusText.setText("Starter Google innlogging..."); Intent signInIntent = mGoogleSignInClient.getSignInIntent(); signInLauncher.launch(signInIntent); }); return view; } private void handleGoogleResult(Task completedTask) { try { GoogleSignInAccount account = completedTask.getResult(ApiException.class); // 1. Google er OK. Nå logger vi inn på WordPress. statusText.setText("Google OK. Kobler til KBS Intranett..."); signInButton.setEnabled(false); // Hindre dobbeltklikk String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; AuthRepository.loginToWordPress( account.getIdToken(), account.getDisplayName(), account.getEmail(), photoUrl, new AuthRepository.AuthCallback() { @Override public void onSuccess(String role) { // 2. Alt er OK! Naviger til Hjem. if (isAdded()) { statusText.setText("Innlogging OK!"); NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); navController.navigate(R.id.action_login_to_home); } } @Override public void onError(String message) { if (isAdded()) { statusText.setText(message); signInButton.setEnabled(true); } } } ); } catch (ApiException e) { // --- KORRIGERT FEILMELDING --- Log.w(TAG, "signInResult:failed code=" + e.getStatusCode()); String message; if (e.getStatusCode() == 12500) { message = "Konto ikke funnet, eller konto uten rettigheter."; } else { message = "Google-feil: " + e.getStatusCode(); } statusText.setText(message); // --- SLUTT PÅ KORRIGERING --- } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginRequest.java ============================================================ package com.kbs.kbsintranett; public class LoginRequest { public String token; public LoginRequest(String token) { this.token = token; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; public class LoginResponse { public boolean success; @SerializedName("full_cookie") public String fullCookie; public String role; @SerializedName("user_id") public int userId; // --- NYE FELTER --- @SerializedName("first_name") public String firstName; @SerializedName("last_name") public String lastName; @SerializedName("stilling") // Sjekk at JSON-nøkkelen fra WP matcher dette public String stilling; @SerializedName("mobiltelefon") // Sjekk at JSON-nøkkelen fra WP matcher dette public String mobiltelefon; // ------------------ public String message; } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java ============================================================ package com.kbs.kbsintranett; import android.os.Bundle; import android.util.Log; import android.view.View; import androidx.appcompat.app.AppCompatActivity; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.material.bottomnavigation.BottomNavigationView; public class MainActivity extends AppCompatActivity { // VIKTIG: Erstatt denne med din Web Client ID public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; private static final String TAG = "MainActivity"; private NavController navController; private BottomNavigationView bottomNav; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 1. Setup UI bottomNav = findViewById(R.id.bottom_nav_view); NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); if (navHostFragment != null) { navController = navHostFragment.getNavController(); NavigationUI.setupWithNavController(bottomNav, navController); // Skjul meny på login-skjerm navController.addOnDestinationChangedListener((controller, destination, arguments) -> { // Sjekker mot R.id.navigation_login som er ID'en til fragmentet if (destination.getId() == R.id.navigation_login) { bottomNav.setVisibility(View.GONE); } else { bottomNav.setVisibility(View.VISIBLE); } }); } // 2. Start Silent Sign-In ved oppstart checkLoginState(); } private void checkLoginState() { GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); if (account == null) { navigateToLogin(); } else { refreshGoogleToken(); } } private void refreshGoogleToken() { GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(GOOGLE_WEB_CLIENT_ID) .requestEmail() .build(); GoogleSignInClient client = GoogleSignIn.getClient(this, gso); client.silentSignIn() .addOnSuccessListener(account -> { String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; AuthRepository.loginToWordPress( account.getIdToken(), account.getDisplayName(), account.getEmail(), photoUrl, new AuthRepository.AuthCallback() { @Override public void onSuccess(String role) { Log.d(TAG, "Silent login fullført. Rolle: " + role); // Gå videre til Home hvis vi står på Login if (navController != null && navController.getCurrentDestination() != null && navController.getCurrentDestination().getId() == R.id.navigation_login) { // Denne aksjonen finnes i mobile_navigation.xml navController.navigate(R.id.action_login_to_home); } } @Override public void onError(String message) { Log.e(TAG, "Silent login feilet mot WP: " + message); navigateToLogin(); } } ); }) .addOnFailureListener(e -> { Log.e(TAG, "Silent Sign-In feilet mot Google", e); navigateToLogin(); }); } private void navigateToLogin() { if (navController != null) { if (navController.getCurrentDestination() != null && // Sjekker mot R.id.navigation_login som er ID'en til fragmentet navController.getCurrentDestination().getId() != R.id.navigation_login) { // Denne ID'en finnes i mobile_navigation.xml navController.navigate(R.id.navigation_login); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsAdapter.java ============================================================ package com.kbs.kbsintranett; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class NewsAdapter extends RecyclerView.Adapter { private List newsList; public NewsAdapter(List newsList) { this.newsList = newsList; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { NewsItem item = newsList.get(position); holder.title.setText(item.getTitle()); holder.excerpt.setText(item.getExcerpt()); holder.author.setText("Av: " + item.getAuthor()); } @Override public int getItemCount() { return newsList.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView title, excerpt, author; public ViewHolder(View view) { super(view); title = view.findViewById(R.id.news_title); excerpt = view.findViewById(R.id.news_excerpt); author = view.findViewById(R.id.news_author); } } } ============================================================ 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>(){}.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> getPosts(); // 2. Hent et spesifikt skjema med ID @GET("wp-json/gf/v2/forms/{id}") Call getForm(@Path("id") int formId); // 3. SEND INN SKJEMA (JSON-data uten filer) @POST("wp-json/gf/v2/forms/{id}/submissions") Call submitForm(@Path("id") int formId, @Body FormSubmission submission); // 4. LOGIN MED GOOGLE @POST("wp-json/kbs/v1/login") Call googleLogin(@Body LoginRequest request); // 5. HENT LISTE AV SKJEMAER @GET("wp-json/gf/v2/forms") Call> getFormsListMap(); // 6. SEND INN SKJEMA (MULTIPART - for filopplasting) @Multipart @POST("wp-json/gf/v2/forms/{id}/submissions") Call submitMultipartForm( @Path("id") int formId, @PartMap Map textFields, @Part List files ); // 7. HENT KALENDERHENDELSER @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); // 8. HENT INNSENDINGER (Entries) - NYTT @GET("wp-json/gf/v2/entries") Call getEntries( @Query("form_ids") int formId, @Query("search") String searchJson, @Query("paging[page_size]") int pageSize ); } ============================================================ 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

) 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 ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_form.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_home.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_launcher_background.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\activity_main.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\fragment_forms.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\fragment_handbook.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\fragment_home.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\fragment_login.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\fragment_profile.xml ============================================================