Før lenker til tidligere oppføriner

This commit is contained in:
ErolHaagenrud 2025-12-11 12:13:06 +01:00
parent 4e67902021
commit c23b4ba3d3
6 changed files with 867 additions and 535 deletions

View file

@ -1,15 +1,19 @@
package com.kbs.kbsintranett;
import android.Manifest;
import android.animation.LayoutTransition;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.Html;
@ -18,12 +22,14 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
@ -37,6 +43,8 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.google.gson.JsonElement;
@ -46,6 +54,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -92,9 +101,11 @@ public class FormsFragment extends Fragment {
private LinearLayout formContainer;
private LinearLayout historyContainer;
private View historyWrapper; // Wrapper for historikk-modulen
private TextView txtStatus;
private TextView lblHistory;
private ProgressBar loadingSpinner;
private ImageView btnToggleHistory; // NY: Knapp for å vise/skjule historikk
// --- HOVEDSKJEMA STATE ---
private Map<String, View> fieldWrappers = new HashMap<>();
@ -110,9 +121,15 @@ public class FormsFragment extends Fragment {
private List<NestedEntry> nestedEntries = new ArrayList<>();
private LinearLayout nestedEntriesContainer;
private TextView totalAmountView;
// --- FILOPPLASTING & KAMERA ---
private String pendingFileFieldId = null;
private boolean isSelectingForChild = false;
private Uri currentPhotoUri = null;
private ActivityResultLauncher<Intent> filePickerLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
private ActivityResultLauncher<String> requestPermissionLauncher;
private GravityForm currentForm;
private final OkHttpClient client = new OkHttpClient();
@ -131,8 +148,26 @@ public class FormsFragment extends Fragment {
handleFileSelection(pendingFileFieldId, uri, isSelectingForChild);
}
}
pendingFileFieldId = null;
isSelectingForChild = false;
}
);
takePictureLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success && currentPhotoUri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild);
} else if (!success) {
currentPhotoUri = null;
}
}
);
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
openCamera();
} else {
Toast.makeText(getContext(), "Kameratillatelse er påkrevd for å ta bilde.", Toast.LENGTH_LONG).show();
}
}
);
}
@ -143,10 +178,29 @@ public class FormsFragment extends Fragment {
View view = inflater.inflate(R.layout.fragment_forms, container, false);
formContainer = view.findViewById(R.id.form_container);
historyContainer = view.findViewById(R.id.historyContainer);
historyWrapper = view.findViewById(R.id.history_wrapper);
txtStatus = view.findViewById(R.id.txt_status);
lblHistory = view.findViewById(R.id.lbl_history);
loadingSpinner = view.findViewById(R.id.loading_spinner);
// --- NY KODE START: Vis/Skjul knapp ---
btnToggleHistory = view.findViewById(R.id.btn_toggle_history);
if (btnToggleHistory != null) {
btnToggleHistory.setOnClickListener(v -> toggleHistoryVisibility());
}
// --- NY KODE SLUTT ---
// --- FIKS FOR NULLPOINTER EXCEPTION LAYOUTTRANSITION ---
if (view instanceof ViewGroup) {
LayoutTransition transition = ((ViewGroup) view).getLayoutTransition();
if (transition == null) {
transition = new LayoutTransition();
((ViewGroup) view).setLayoutTransition(transition);
}
transition.enableTransitionType(LayoutTransition.CHANGING);
}
// ----------------------------------------------------------
if (formContainer == null) {
formContainer = new LinearLayout(getContext());
}
@ -161,6 +215,58 @@ public class FormsFragment extends Fragment {
return view;
}
// --- UI LOGIKK FOR DELT SKJERM ---
// Kalles når brukeren interagerer med et felt i skjemaet
private void expandFormModule() {
if (historyWrapper != null && historyWrapper.getVisibility() == View.VISIBLE) {
historyWrapper.setVisibility(View.GONE);
// Oppdater ikonet til å peke NED (for å vise at man kan hente historikk ned igjen)
if (btnToggleHistory != null) {
btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float);
}
}
}
// NY METODE: Håndterer klikk pil-knappen
private void toggleHistoryVisibility() {
if (historyWrapper == null || btnToggleHistory == null) return;
if (historyWrapper.getVisibility() == View.VISIBLE) {
// Skjul historikk ( til fullskjerm)
historyWrapper.setVisibility(View.GONE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float);
} else {
// Vis historikk ( til splitscreen)
historyWrapper.setVisibility(View.VISIBLE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float);
}
}
private void attachInteractionListener(View view) {
if (view == null) return;
// Touch listener fanger opp klikk før tastaturet kommer opp
view.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false; // Return false to allow normal processing
});
// Focus listener for edittexts etc
view.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
expandFormModule();
}
});
// Click listener for buttons/checkboxes
if (view.isClickable()) {
view.setOnClickListener(v -> expandFormModule());
}
}
// ----------------------------------
private void fetchFormStructure() {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE);
updateStatus("Laster skjema...");
@ -202,6 +308,12 @@ public class FormsFragment extends Fragment {
nestedEntries.clear();
updateStatus("");
// Reset visibility of history on new load (Splitscreen start)
if (historyWrapper != null) {
historyWrapper.setVisibility(View.VISIBLE);
if (btnToggleHistory != null) btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float);
}
TextView title = new TextView(getContext());
title.setText(getCleanTitle(form.title));
title.setTextSize(24);
@ -219,7 +331,6 @@ public class FormsFragment extends Fragment {
}
if (form.fields == null) return;
for (GravityField field : form.fields) {
if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) {
continue;
@ -326,6 +437,7 @@ public class FormsFragment extends Fragment {
btnAdd.setBackgroundColor(Color.parseColor("#53AFE9"));
btnAdd.setTextColor(Color.WHITE);
btnAdd.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
int childFormId = 18;
if (field.gpnfForm != null) {
try {
@ -358,7 +470,6 @@ public class FormsFragment extends Fragment {
.setMessage("Laster skjema...")
.setCancelable(false)
.show();
RetrofitClient.getApiService().getForm(childFormId).enqueue(new retrofit2.Callback<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> response) {
@ -380,8 +491,6 @@ public class FormsFragment extends Fragment {
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();
@ -391,10 +500,8 @@ public class FormsFragment extends Fragment {
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);
@ -405,7 +512,6 @@ public class FormsFragment extends Fragment {
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)) {
@ -479,7 +585,6 @@ public class FormsFragment extends Fragment {
// 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 {
@ -542,27 +647,20 @@ public class FormsFragment extends Fragment {
}
}
// --- FELLES METODER ---
// --- FELLES METODER (FILE UPLOAD M/ CAMERA STØTTE) ---
private void renderFileUploadField(LinearLayout container, GravityField field, Map<String, View> viewsMap, Map<String, Boolean> reqMap, boolean isChild) {
LinearLayout fileLayout = new LinearLayout(getContext());
fileLayout.setOrientation(LinearLayout.HORIZONTAL);
Button btnUpload = new Button(getContext());
btnUpload.setText("Velg fil");
btnUpload.setText("Velg fil / Ta bilde");
btnUpload.setOnClickListener(v -> {
if (filePickerLauncher == null) return;
try {
// Setter state før vi viser dialog
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();
}
expandFormModule(); // Trigger expand
showFileSourceDialog();
});
TextView txtFileName = new TextView(getContext());
txtFileName.setText("Ingen fil valgt");
@ -578,6 +676,62 @@ public class FormsFragment extends Fragment {
reqMap.put(field.id, field.isRequired);
}
// Hjelpemetode for å vise dialog
private void showFileSourceDialog() {
String[] options = {"Ta bilde", "Velg fil"};
new AlertDialog.Builder(getContext())
.setTitle("Last opp vedlegg")
.setItems(options, (dialog, which) -> {
if (which == 0) {
// Ta bilde - SJEKKER PERMISSION FØRST
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
// Spør om lov
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
}
} else {
// Velg fil
if (filePickerLauncher != null) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
String[] mimeTypes = {"image/jpeg", "image/png", "application/pdf"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
filePickerLauncher.launch(intent);
}
}
})
.show();
}
private void openCamera() {
currentPhotoUri = createImageUri();
if (currentPhotoUri != null && takePictureLauncher != null) {
try {
takePictureLauncher.launch(currentPhotoUri);
} catch (Exception e) {
Toast.makeText(getContext(), "Kunne ikke starte kamera: " + e.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Camera launch failed", e);
}
} else {
Toast.makeText(getContext(), "Kunne ikke opprette bildefil", Toast.LENGTH_SHORT).show();
}
}
private Uri createImageUri() {
try {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private void handleFileSelection(String fieldId, Uri uri, boolean isChild) {
if (isChild) {
childFileUploads.put(fieldId, uri);
@ -631,6 +785,7 @@ public class FormsFragment extends Fragment {
timeInput.setClickable(true);
timeInput.setHint("00:00");
timeInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar mcurrentTime = Calendar.getInstance();
int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY);
int minute = mcurrentTime.get(Calendar.MINUTE);
@ -671,6 +826,8 @@ public class FormsFragment extends Fragment {
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); }
});
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
@ -682,6 +839,9 @@ public class FormsFragment extends Fragment {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
input.setMinLines(3);
input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START);
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
@ -694,10 +854,19 @@ public class FormsFragment extends Fragment {
RadioButton rb = new RadioButton(getContext());
rb.setText(choice.text);
rb.setTag(choice.value);
// Also trigger expand on RadioButton click
rb.setOnClickListener(v -> {
expandFormModule();
evaluateAllConditionalLogic();
});
group.addView(rb);
}
}
group.setOnCheckedChangeListener((g, i) -> evaluateAllConditionalLogic());
// Fallback listener
group.setOnCheckedChangeListener((g, i) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(group);
views.put(field.id, group);
req.put(field.id, field.isRequired);
@ -705,6 +874,13 @@ public class FormsFragment extends Fragment {
private void renderSelectField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
Spinner spinner = new Spinner(getContext());
// Spinner touch listener is tricky, usually set onTouchListener works
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false;
});
List<String> labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) {
@ -723,10 +899,11 @@ public class FormsFragment extends Fragment {
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());
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputId, checkBox);
req.put(inputId, field.isRequired);
@ -739,17 +916,16 @@ public class FormsFragment extends Fragment {
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
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());
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputDef.id, checkBox);
req.put(inputDef.id, false);
@ -776,6 +952,7 @@ public class FormsFragment extends Fragment {
dateInput.setClickable(true);
dateInput.setHint("dd.mm.yyyy");
dateInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar c = Calendar.getInstance();
new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> {
dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year));
@ -827,6 +1004,7 @@ public class FormsFragment extends Fragment {
else if (lowerSub.contains("etternavn")) subInput.setText(user.getLastName());
}
attachInteractionListener(subInput);
container.addView(subInput);
views.put(subField.id, subInput);
req.put(subField.id, isSubRequired);
@ -895,7 +1073,8 @@ public class FormsFragment extends Fragment {
aggregatedResult = aggregatedResult && ruleMatch;
if (!aggregatedResult) break;
} else {
aggregatedResult = aggregatedResult || ruleMatch;
aggregatedResult = aggregatedResult ||
ruleMatch;
if (aggregatedResult) break;
}
}
@ -936,16 +1115,14 @@ public class FormsFragment extends Fragment {
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 cb.getTag() != null ?
cb.getTag().toString() : "1";
}
return "";
}
// ----------------------------------------
return "";
}
@ -989,7 +1166,6 @@ public class FormsFragment extends Fragment {
}
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);
@ -1035,10 +1211,9 @@ public class FormsFragment extends Fragment {
});
} 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
updateStatus("Feil (" + response.code() + "): " + errBody);
} catch(Exception e){}
}
}
@ -1046,7 +1221,6 @@ public class FormsFragment extends Fragment {
}
}
// Konverterer dd.MM.yyyy -> yyyy-MM-dd
private String formatDateForApi(String dateStr) {
if (dateStr == null || dateStr.isEmpty()) return "";
try {
@ -1055,7 +1229,7 @@ public class FormsFragment extends Fragment {
SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
return apiFormat.format(date);
} catch (Exception e) {
return dateStr; // Fallback hvis formatet er ukjent
return dateStr;
}
}
@ -1110,13 +1284,16 @@ public class FormsFragment extends Fragment {
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 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); }
try (Source source = Okio.source(inputStream)) { sink.writeAll(source);
}
}
};
return MultipartBody.Part.createFormData(partName, fileName, requestBody);
} catch (Exception e) { return null; }
} catch (Exception e) { return null;
}
}
private void fetchFormEntries() {
@ -1151,7 +1328,8 @@ public class FormsFragment extends Fragment {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
showHistory(entries);
if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0) {
if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0)
{
try {
prefillFormFromHistory(entries.getJSONObject(0));
} catch (JSONException e) { e.printStackTrace(); }
@ -1159,7 +1337,8 @@ public class FormsFragment extends Fragment {
});
}
}
} catch (JSONException e) { e.printStackTrace(); }
} catch (JSONException e) { e.printStackTrace();
}
}
}
});
@ -1177,14 +1356,21 @@ public class FormsFragment extends Fragment {
}
try {
int count = Math.min(entries.length(), 5);
// Vis flere oppføringer siden vi har scrolle-mulighet øverst
int count = Math.min(entries.length(), 20);
for (int i = 0; i < count; i++) {
JSONObject entry = entries.getJSONObject(i);
String date = entry.optString("date_created");
// Prøv å finne en bedre tittel enn bare dato (f.eks Prosjektnavn eller Sted)
String titleText = "Innsendt: " + date;
TextView item = new TextView(getContext());
item.setText("Innsendt: " + date);
item.setText(titleText);
item.setPadding(10, 20, 10, 20);
item.setBackgroundResource(android.R.drawable.list_selector_background);
item.setTextSize(14);
// Add click listener to show details
item.setOnClickListener(v -> showEntryDetails(entry));
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
line.setBackgroundColor(Color.LTGRAY);
@ -1192,7 +1378,64 @@ public class FormsFragment extends Fragment {
historyContainer.addView(item);
historyContainer.addView(line);
}
} catch (JSONException e) { e.printStackTrace(); }
} catch (JSONException e) { e.printStackTrace();
}
}
// NY METODE: Vis detaljer i dialog
private void showEntryDetails(JSONObject entry) {
StringBuilder details = new StringBuilder();
try {
// Loop gjennom alle felter i entry og match med skjema-definisjon
// Merk: Entry keys er felt-IDer (f.eks "1", "3.2")
// Dato
details.append("<b>Innsendt:</b> ").append(entry.optString("date_created")).append("<br><br>");
// Iterer gjennom feltene i skjema-definisjonen for å riktig rekkefølge
if (currentForm != null && currentForm.fields != null) {
for (GravityField field : currentForm.fields) {
if ("section".equals(field.type) || "html".equals(field.type) || "captcha".equals(field.type)) continue;
String value = "";
if (field.inputs != null && !field.inputs.isEmpty()) {
// Composite fields (Address, Name)
for (GravityField input : field.inputs) {
String subVal = entry.optString(input.id);
if (!subVal.isEmpty()) {
value += " " + subVal;
}
}
} else {
// Standard field
value = entry.optString(String.valueOf(field.id));
}
if (!value.trim().isEmpty()) {
// For filopplastinger er verdien ofte en URL.
if ("fileupload".equals(field.type)) {
value = "(Vedlegg)";
}
details.append("<b>").append(field.label).append(":</b><br>")
.append(value).append("<br><br>");
}
}
}
} catch (Exception e) {
details.append("Kunne ikke vise detaljer.");
}
ScrollView scroll = new ScrollView(getContext());
TextView text = new TextView(getContext());
text.setText(Html.fromHtml(details.toString(), Html.FROM_HTML_MODE_COMPACT));
text.setPadding(40, 40, 40, 40);
scroll.addView(text);
new AlertDialog.Builder(getContext())
.setTitle("Detaljer")
.setView(scroll)
.setPositiveButton("Lukk", null)
.show();
}
private void prefillFormFromHistory(JSONObject latestEntry) {
@ -1239,8 +1482,6 @@ public class FormsFragment extends Fragment {
} 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");
@ -1253,12 +1494,12 @@ public class FormsFragment extends Fragment {
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; }
NestedEntry(String id, String d, String p) { this.id = id; this.description = d; this.price = p;
}
}
private void updateStatus(String msg) {

View file

@ -2,194 +2,60 @@ 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 view = inflater.inflate(R.layout.fragment_forms_list, container, false);
LinearLayout formsContainer = view.findViewById(R.id.forms_container);
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);
}
}
}
// Legger til knappene for de ulike skjemaene
addFormButton(formsContainer, "1. Ansatteopplysninger", 1);
addFormButton(formsContainer, "4. RUH (Rapport om uønsket hendelse)", 4);
addFormButton(formsContainer, "9. Sikkerhetskurs / Kompetansebevis", 9);
addFormButton(formsContainer, "10. HMS-bekreftelse", 10);
addFormButton(formsContainer, "11. Egenmelding", 11);
addFormButton(formsContainer, "12. Sjekkliste for firmabil", 12);
addFormButton(formsContainer, "14. SJA (Sikker Jobbanalyse)", 14);
addFormButton(formsContainer, "15. Fraværsvarsel", 15);
addFormButton(formsContainer, "16. Refusjon utlegg", 16);
addFormButton(formsContainer, "21. Forberedelse til medarbeidersamtale", 21);
addFormButton(formsContainer, "22. Medarbeiderundersøkelse", 22);
return view;
}
@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<Map<String, GravityForm>>() {
@Override
public void onResponse(Call<Map<String, GravityForm>> call, Response<Map<String, GravityForm>> response) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (response.isSuccessful() && response.body() != null) {
Map<String, GravityForm> formMap = response.body();
if (formMap.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet.");
return;
}
List<GravityForm> visibleForms = new ArrayList<>();
for (GravityForm form : formMap.values()) {
// Sjekk om aktiv
boolean isActive = form.isActive == null || !"0".equals(form.isActive);
// Sjekk om vi finner et sorteringstall i tittelen
Integer sortOrder = getSortOrderFromTitle(form.title);
if (form != null && form.title != null && isActive && sortOrder != null) {
visibleForms.add(form);
}
}
if (visibleForms.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen tilgjengelige skjemaer (sjekk nummerering i titler).");
return;
}
// Sorter listen basert tallet i tittelen
Collections.sort(visibleForms, new Comparator<GravityForm>() {
@Override
public int compare(GravityForm f1, GravityForm f2) {
return Integer.compare(getSortOrderFromTitle(f1.title), getSortOrderFromTitle(f2.title));
}
});
for (GravityForm form : visibleForms) {
addFormButton(form);
}
} else {
if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")");
}
}
@Override
public void onFailure(Call<Map<String, GravityForm>> call, Throwable t) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage());
}
});
}
/**
* 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 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 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);
});
private void addFormButton(LinearLayout container, String title, int formId) {
Button btn = new Button(getContext());
btn.setText(title);
btn.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå
btn.setTextColor(Color.WHITE);
btn.setPadding(30, 30, 30, 30);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, 0, 20);
button.setLayoutParams(params);
ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // Litt avstand mellom knappene
btn.setLayoutParams(params);
container.addView(button);
btn.setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putInt("formId", formId);
// HER VAR FEILEN: Endret R.id.nav_forms til riktig action ID
Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle);
});
container.addView(btn);
}
}

View file

@ -1,74 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@android:color/white">
android:orientation="vertical"
android:background="#F5F5F5"
tools:context=".FormsFragment">
<LinearLayout
android:id="@+id/main_layout"
android:id="@+id/history_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjema"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
android:layout_height="0dp"
android:layout_weight="3"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp"
android:layout_marginBottom="8dp"
android:padding="16dp">
<TextView
android:id="@+id/lbl_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tidligere innsendinger:"
android:text="Tidligere innsendinger"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"/>
android:textColor="#333333"
android:layout_marginBottom="10dp"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/historyContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="20dp"/>
android:orientation="vertical" />
</ScrollView>
</LinearLayout>
<View
<LinearLayout
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#0069B3"
android:layout_marginBottom="20dp"/>
android:layout_height="0dp"
android:layout_weight="7"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FAFAFA">
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
android:layout_marginEnd="8dp"/>
<TextView
android:layout_width="wrap_content"
android:id="@+id/txt_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Ny innsending:"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="10dp"/>
android:layout_weight="1"
android:textColor="#666666"
android:textSize="12sp"
android:text="" />
<ImageView
android:id="@+id/btn_toggle_history"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/arrow_up_float"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:contentDescription="Vis/Skjul historikk"
app:tint="#666666" />
</LinearLayout>
<ScrollView
android:id="@+id/form_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fillViewport="true">
<LinearLayout
android:id="@+id/form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<TextView
android:id="@+id/txt_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="#D32F2F"
android:gravity="center" />
android:orientation="vertical"
android:paddingBottom="40dp">
</LinearLayout>
</ScrollView>
</ScrollView>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout" android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#F5F5F5">
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjemaer"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="20dp"
android:textColor="#333333"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/forms_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</LinearLayout>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="my_images" path="Pictures" />
</paths>

View file

@ -198,6 +198,17 @@ FILSTI: app\src\main\AndroidManifest.xml
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@ -437,16 +448,20 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsFragment.java
============================================================
package com.kbs.kbsintranett;
import android.Manifest;
import android.animation.LayoutTransition;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.Html;
@ -455,6 +470,7 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
@ -474,6 +490,8 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.google.gson.JsonElement;
@ -483,6 +501,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -529,6 +548,7 @@ public class FormsFragment extends Fragment {
private LinearLayout formContainer;
private LinearLayout historyContainer;
private View historyWrapper; // Wrapper for historikk-modulen som skal skjules
private TextView txtStatus;
private TextView lblHistory;
private ProgressBar loadingSpinner;
@ -547,9 +567,15 @@ public class FormsFragment extends Fragment {
private List<NestedEntry> nestedEntries = new ArrayList<>();
private LinearLayout nestedEntriesContainer;
private TextView totalAmountView;
// --- FILOPPLASTING & KAMERA ---
private String pendingFileFieldId = null;
private boolean isSelectingForChild = false;
private Uri currentPhotoUri = null;
private ActivityResultLauncher<Intent> filePickerLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
private ActivityResultLauncher<String> requestPermissionLauncher;
private GravityForm currentForm;
private final OkHttpClient client = new OkHttpClient();
@ -559,6 +585,7 @@ public class FormsFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
@ -568,8 +595,28 @@ public class FormsFragment extends Fragment {
handleFileSelection(pendingFileFieldId, uri, isSelectingForChild);
}
}
pendingFileFieldId = null;
isSelectingForChild = false;
}
);
takePictureLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success && currentPhotoUri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild);
} else if (!success) {
currentPhotoUri = null;
}
}
);
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
openCamera();
} else {
Toast.makeText(getContext(), "Kameratillatelse er påkrevd for å ta bilde.", Toast.LENGTH_LONG).show();
}
}
);
}
@ -578,12 +625,26 @@ public class FormsFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_forms, container, false);
formContainer = view.findViewById(R.id.form_container);
historyContainer = view.findViewById(R.id.historyContainer);
historyWrapper = view.findViewById(R.id.history_wrapper); // Ny referanse
txtStatus = view.findViewById(R.id.txt_status);
lblHistory = view.findViewById(R.id.lbl_history);
loadingSpinner = view.findViewById(R.id.loading_spinner);
// --- FIKS FOR NULLPOINTER EXCEPTION ---
// Vi sjekker om LayoutTransition finnes, hvis ikke lager vi en ny.
if (view instanceof ViewGroup) {
LayoutTransition transition = ((ViewGroup) view).getLayoutTransition();
if (transition == null) {
transition = new LayoutTransition();
((ViewGroup) view).setLayoutTransition(transition);
}
transition.enableTransitionType(LayoutTransition.CHANGING);
}
// --------------------------------------
if (formContainer == null) {
formContainer = new LinearLayout(getContext());
}
@ -598,6 +659,41 @@ public class FormsFragment extends Fragment {
return view;
}
// --- UI LOGIKK FOR DELT SKJERM ---
// Kalles når brukeren interagerer med et felt i skjemaet
private void expandFormModule() {
if (historyWrapper != null && historyWrapper.getVisibility() == View.VISIBLE) {
historyWrapper.setVisibility(View.GONE);
}
}
private void attachInteractionListener(View view) {
if (view == null) return;
// Touch listener fanger opp klikk før tastaturet kommer opp
view.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false; // Return false to allow normal processing
});
// Focus listener for edittexts etc
view.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
expandFormModule();
}
});
// Click listener for buttons/checkboxes
if (view.isClickable()) {
view.setOnClickListener(v -> expandFormModule());
}
}
// ----------------------------------
private void fetchFormStructure() {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE);
updateStatus("Laster skjema...");
@ -639,6 +735,9 @@ public class FormsFragment extends Fragment {
nestedEntries.clear();
updateStatus("");
// Reset visibility of history on new load
if (historyWrapper != null) historyWrapper.setVisibility(View.VISIBLE);
TextView title = new TextView(getContext());
title.setText(getCleanTitle(form.title));
title.setTextSize(24);
@ -763,6 +862,7 @@ public class FormsFragment extends Fragment {
btnAdd.setBackgroundColor(Color.parseColor("#53AFE9"));
btnAdd.setTextColor(Color.WHITE);
btnAdd.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
int childFormId = 18;
if (field.gpnfForm != null) {
try {
@ -817,7 +917,6 @@ public class FormsFragment extends Fragment {
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();
@ -979,27 +1078,20 @@ public class FormsFragment extends Fragment {
}
}
// --- FELLES METODER ---
// --- FELLES METODER (FILE UPLOAD M/ CAMERA STØTTE) ---
private void renderFileUploadField(LinearLayout container, GravityField field, Map<String, View> viewsMap, Map<String, Boolean> reqMap, boolean isChild) {
LinearLayout fileLayout = new LinearLayout(getContext());
fileLayout.setOrientation(LinearLayout.HORIZONTAL);
Button btnUpload = new Button(getContext());
btnUpload.setText("Velg fil");
btnUpload.setText("Velg fil / Ta bilde");
btnUpload.setOnClickListener(v -> {
if (filePickerLauncher == null) return;
try {
// Setter state før vi viser dialog
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();
}
expandFormModule(); // Trigger expand
showFileSourceDialog();
});
TextView txtFileName = new TextView(getContext());
txtFileName.setText("Ingen fil valgt");
@ -1015,6 +1107,63 @@ public class FormsFragment extends Fragment {
reqMap.put(field.id, field.isRequired);
}
// Hjelpemetode for å vise dialog
private void showFileSourceDialog() {
String[] options = {"Ta bilde", "Velg fil"};
new AlertDialog.Builder(getContext())
.setTitle("Last opp vedlegg")
.setItems(options, (dialog, which) -> {
if (which == 0) {
// Ta bilde - SJEKKER PERMISSION FØRST
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
// Spør om lov
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
}
} else {
// Velg fil
if (filePickerLauncher != null) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
String[] mimeTypes = {"image/jpeg", "image/png", "application/pdf"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
filePickerLauncher.launch(intent);
}
}
})
.show();
}
private void openCamera() {
currentPhotoUri = createImageUri();
if (currentPhotoUri != null && takePictureLauncher != null) {
try {
takePictureLauncher.launch(currentPhotoUri);
} catch (Exception e) {
Toast.makeText(getContext(), "Kunne ikke starte kamera: " + e.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Camera launch failed", e);
}
} else {
Toast.makeText(getContext(), "Kunne ikke opprette bildefil", Toast.LENGTH_SHORT).show();
}
}
private Uri createImageUri() {
try {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private void handleFileSelection(String fieldId, Uri uri, boolean isChild) {
if (isChild) {
childFileUploads.put(fieldId, uri);
@ -1068,6 +1217,7 @@ public class FormsFragment extends Fragment {
timeInput.setClickable(true);
timeInput.setHint("00:00");
timeInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar mcurrentTime = Calendar.getInstance();
int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY);
int minute = mcurrentTime.get(Calendar.MINUTE);
@ -1108,6 +1258,9 @@ public class FormsFragment extends Fragment {
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); }
});
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
@ -1119,6 +1272,9 @@ public class FormsFragment extends Fragment {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
input.setMinLines(3);
input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START);
attachInteractionListener(input); // Add listener
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
@ -1131,10 +1287,20 @@ public class FormsFragment extends Fragment {
RadioButton rb = new RadioButton(getContext());
rb.setText(choice.text);
rb.setTag(choice.value);
// Also trigger expand on RadioButton click
rb.setOnClickListener(v -> {
expandFormModule();
evaluateAllConditionalLogic();
});
group.addView(rb);
}
}
group.setOnCheckedChangeListener((g, i) -> evaluateAllConditionalLogic());
// Fallback listener
group.setOnCheckedChangeListener((g, i) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(group);
views.put(field.id, group);
req.put(field.id, field.isRequired);
@ -1142,6 +1308,14 @@ public class FormsFragment extends Fragment {
private void renderSelectField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
Spinner spinner = new Spinner(getContext());
// Spinner touch listener is tricky, usually set onTouchListener works
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false;
});
List<String> labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) {
@ -1160,10 +1334,13 @@ public class FormsFragment extends Fragment {
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());
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputId, checkBox);
req.put(inputId, field.isRequired);
@ -1176,17 +1353,16 @@ public class FormsFragment extends Fragment {
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());
checkBox.setOnCheckedChangeListener((b, c) -> {
expandFormModule();
evaluateAllConditionalLogic();
});
container.addView(checkBox);
views.put(inputDef.id, checkBox);
req.put(inputDef.id, false);
@ -1213,6 +1389,7 @@ public class FormsFragment extends Fragment {
dateInput.setClickable(true);
dateInput.setHint("dd.mm.yyyy");
dateInput.setOnClickListener(v -> {
expandFormModule(); // Trigger expand
Calendar c = Calendar.getInstance();
new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> {
dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year));
@ -1264,6 +1441,8 @@ public class FormsFragment extends Fragment {
else if (lowerSub.contains("etternavn")) subInput.setText(user.getLastName());
}
attachInteractionListener(subInput);
container.addView(subInput);
views.put(subField.id, subInput);
req.put(subField.id, isSubRequired);
@ -1373,16 +1552,13 @@ public class FormsFragment extends Fragment {
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 "";
}
@ -1426,7 +1602,6 @@ public class FormsFragment extends Fragment {
}
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);
@ -1472,10 +1647,9 @@ public class FormsFragment extends Fragment {
});
} 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
updateStatus("Feil (" + response.code() + "): " + errBody);
} catch(Exception e){}
}
}
@ -1483,7 +1657,6 @@ public class FormsFragment extends Fragment {
}
}
// Konverterer dd.MM.yyyy -> yyyy-MM-dd
private String formatDateForApi(String dateStr) {
if (dateStr == null || dateStr.isEmpty()) return "";
try {
@ -1492,7 +1665,7 @@ public class FormsFragment extends Fragment {
SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
return apiFormat.format(date);
} catch (Exception e) {
return dateStr; // Fallback hvis formatet er ukjent
return dateStr;
}
}
@ -1614,14 +1787,24 @@ public class FormsFragment extends Fragment {
}
try {
int count = Math.min(entries.length(), 5);
// Vis flere oppføringer siden vi nå har scrolle-mulighet øverst
int count = Math.min(entries.length(), 20);
for (int i = 0; i < count; i++) {
JSONObject entry = entries.getJSONObject(i);
String date = entry.optString("date_created");
// Prøv å finne en bedre tittel enn bare dato (f.eks Prosjektnavn eller Sted)
String titleText = "Innsendt: " + date;
TextView item = new TextView(getContext());
item.setText("Innsendt: " + date);
item.setText(titleText);
item.setPadding(10, 20, 10, 20);
item.setBackgroundResource(android.R.drawable.list_selector_background);
item.setTextSize(14);
// Add click listener to show details
item.setOnClickListener(v -> showEntryDetails(entry));
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
line.setBackgroundColor(Color.LTGRAY);
@ -1632,6 +1815,64 @@ public class FormsFragment extends Fragment {
} catch (JSONException e) { e.printStackTrace(); }
}
// NY METODE: Vis detaljer i dialog
private void showEntryDetails(JSONObject entry) {
StringBuilder details = new StringBuilder();
try {
// Loop gjennom alle felter i entry og match med skjema-definisjon
// Merk: Entry keys er felt-IDer (f.eks "1", "3.2")
// Dato
details.append("<b>Innsendt:</b> ").append(entry.optString("date_created")).append("<br><br>");
// Iterer gjennom feltene i skjema-definisjonen for å få riktig rekkefølge
if (currentForm != null && currentForm.fields != null) {
for (GravityField field : currentForm.fields) {
if ("section".equals(field.type) || "html".equals(field.type) || "captcha".equals(field.type)) continue;
String value = "";
if (field.inputs != null && !field.inputs.isEmpty()) {
// Composite fields (Address, Name)
for (GravityField input : field.inputs) {
String subVal = entry.optString(input.id);
if (!subVal.isEmpty()) {
value += " " + subVal;
}
}
} else {
// Standard field
value = entry.optString(String.valueOf(field.id));
}
if (!value.trim().isEmpty()) {
// For filopplastinger er verdien ofte en URL.
if ("fileupload".equals(field.type)) {
value = "(Vedlegg)";
}
details.append("<b>").append(field.label).append(":</b><br>")
.append(value).append("<br><br>");
}
}
}
} catch (Exception e) {
details.append("Kunne ikke vise detaljer.");
}
ScrollView scroll = new ScrollView(getContext());
TextView text = new TextView(getContext());
text.setText(Html.fromHtml(details.toString(), Html.FROM_HTML_MODE_COMPACT));
text.setPadding(40, 40, 40, 40);
scroll.addView(text);
new AlertDialog.Builder(getContext())
.setTitle("Detaljer")
.setView(scroll)
.setPositiveButton("Lukk", null)
.show();
}
private void prefillFormFromHistory(JSONObject latestEntry) {
if (latestEntry == null) return;
for (Map.Entry<String, View> entry : inputViews.entrySet()) {
@ -1676,8 +1917,6 @@ public class FormsFragment extends Fragment {
} 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");
@ -1690,7 +1929,6 @@ public class FormsFragment extends Fragment {
if (totalAmountView != null) totalAmountView.setText("Kr 0,00");
}
// --- HJELPEKLASSE FOR NESTED ENTRIES ---
private static class NestedEntry {
String id;
String description;
@ -1712,195 +1950,59 @@ 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 view = inflater.inflate(R.layout.fragment_forms_list, container, false);
LinearLayout formsContainer = view.findViewById(R.id.forms_container);
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);
}
}
}
addFormButton(formsContainer, "1. Ansatteopplysninger", 1);
addFormButton(formsContainer, "4. RUH (Rapport om uønsket hendelse)", 4);
addFormButton(formsContainer, "9. Sikkerhetskurs / Kompetansebevis", 9);
addFormButton(formsContainer, "10. HMS-bekreftelse", 10);
addFormButton(formsContainer, "11. Egenmelding", 11);
addFormButton(formsContainer, "12. Sjekkliste for firmabil", 12);
addFormButton(formsContainer, "14. SJA (Sikker Jobbanalyse)", 14);
addFormButton(formsContainer, "15. Fraværsvarsel", 15);
addFormButton(formsContainer, "16. Refusjon utlegg", 16);
addFormButton(formsContainer, "21. Forberedelse til medarbeidersamtale", 21);
addFormButton(formsContainer, "22. Medarbeiderundersøkelse", 22);
return view;
}
@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<Map<String, GravityForm>>() {
@Override
public void onResponse(Call<Map<String, GravityForm>> call, Response<Map<String, GravityForm>> response) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (response.isSuccessful() && response.body() != null) {
Map<String, GravityForm> formMap = response.body();
if (formMap.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet.");
return;
}
List<GravityForm> visibleForms = new ArrayList<>();
for (GravityForm form : formMap.values()) {
// Sjekk om aktiv
boolean isActive = form.isActive == null || !"0".equals(form.isActive);
// Sjekk om vi finner et sorteringstall i tittelen
Integer sortOrder = getSortOrderFromTitle(form.title);
if (form != null && form.title != null && isActive && sortOrder != null) {
visibleForms.add(form);
}
}
if (visibleForms.isEmpty()) {
if (txtStatus != null) txtStatus.setText("Ingen tilgjengelige skjemaer (sjekk nummerering i titler).");
return;
}
// Sorter listen basert på tallet i tittelen
Collections.sort(visibleForms, new Comparator<GravityForm>() {
@Override
public int compare(GravityForm f1, GravityForm f2) {
return Integer.compare(getSortOrderFromTitle(f1.title), getSortOrderFromTitle(f2.title));
}
});
for (GravityForm form : visibleForms) {
addFormButton(form);
}
} else {
if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")");
}
}
@Override
public void onFailure(Call<Map<String, GravityForm>> call, Throwable t) {
if (getContext() == null) return;
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage());
}
});
}
/**
* 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);
});
private void addFormButton(LinearLayout container, String title, int formId) {
Button btn = new Button(getContext());
btn.setText(title);
btn.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå
btn.setTextColor(Color.WHITE);
btn.setPadding(30, 30, 30, 30);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, 0, 20);
button.setLayoutParams(params);
ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // Litt avstand mellom knappene
btn.setLayoutParams(params);
container.addView(button);
btn.setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putInt("formId", formId);
Navigation.findNavController(v).navigate(R.id.nav_forms, bundle);
});
container.addView(btn);
}
}
@ -3212,79 +3314,130 @@ FILSTI: app\src\main\res\layout\activity_main.xml
FILSTI: app\src\main\res\layout\fragment_forms.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="@android:color/white">
android:orientation="vertical"
android:background="#F5F5F5"
tools:context=".FormsFragment">
<LinearLayout
android:id="@+id/main_layout"
android:id="@+id/history_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjema"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
android:layout_height="0dp"
android:layout_weight="3"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp"
android:layout_marginBottom="8dp"
android:padding="16dp">
<TextView
android:id="@+id/lbl_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tidligere innsendinger:"
android:text="Tidligere innsendinger"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"/>
android:textColor="#333333"
android:layout_marginBottom="10dp"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/historyContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="20dp"/>
android:orientation="vertical" />
</ScrollView>
</LinearLayout>
<View
<LinearLayout
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#0069B3"
android:layout_marginBottom="20dp"/>
android:layout_height="0dp"
android:layout_weight="7"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FAFAFA">
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/txt_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ny innsending:"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="10dp"/>
android:textColor="#666666"
android:textSize="12sp" />
</LinearLayout>
<ScrollView
android:id="@+id/form_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fillViewport="true">
<LinearLayout
android:id="@+id/form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
android:orientation="vertical"
android:paddingBottom="40dp">
</LinearLayout>
</ScrollView>
</LinearLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_forms_list.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout" android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#F5F5F5">
<TextView
android:id="@+id/txt_status"
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjemaer"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="20dp"
android:textColor="#333333"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/forms_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="#D32F2F"
android:gravity="center" />
android:orientation="vertical" />
</ScrollView>
</LinearLayout>
</ScrollView>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_handbook.xml
@ -3836,6 +3989,14 @@ FILSTI: app\src\main\res\xml\data_extraction_rules.xml
-->
</data-extraction-rules>
============================================================
FILSTI: app\src\main\res\xml\file_paths.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="my_images" path="Pictures" />
</paths>
============================================================
FILSTI: app\src\test\java\com\kbs\kbsintranett\ExampleUnitTest.java
============================================================