diff --git a/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java b/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java index 5d0b48f..d68e11a 100644 --- a/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java @@ -1,12 +1,20 @@ 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; @@ -20,35 +28,49 @@ 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 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.Callback; 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 { @@ -57,27 +79,66 @@ public class FormsFragment extends Fragment { // 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; // Overskriften "Tidligere innsendinger" + private TextView lblHistory; private ProgressBar loadingSpinner; - // Vi lagrer View-objektene for å kunne toggle synlighet (Conditional Logic) - // Key = Field ID (f.eks "2", "7.1") - private Map fieldWrappers = new HashMap<>(); // Beholderen (Label + Input) - private Map inputViews = new HashMap<>(); // Selve input-feltet (EditText, RadioGroup, etc) + // --- HOVEDSKJEMA STATE --- + private Map fieldWrappers = new HashMap<>(); + private Map inputViews = new HashMap<>(); private Map requiredFieldsMap = new HashMap<>(); + private Map fileUploads = new HashMap<>(); - private GravityForm currentForm; // Holder referanse til skjemaet for logikk-sjekk + // --- 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) { @@ -139,11 +200,12 @@ public class FormsFragment extends Fragment { fieldWrappers.clear(); inputViews.clear(); requiredFieldsMap.clear(); + fileUploads.clear(); + nestedEntries.clear(); updateStatus(""); - // Tittel TextView title = new TextView(getContext()); - title.setText(form.title); + title.setText(getCleanTitle(form.title)); title.setTextSize(24); title.setTypeface(null, Typeface.BOLD); title.setTextColor(Color.BLACK); @@ -152,37 +214,32 @@ public class FormsFragment extends Fragment { if (form.description != null && !form.description.isEmpty()) { TextView formDesc = new TextView(getContext()); - formDesc.setText(form.description); + String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", ""); + formDesc.setText(cleanDesc); formDesc.setPadding(0, 0, 0, 40); formContainer.addView(formDesc); } if (form.fields == null) return; - // Bygg feltene for (GravityField field : form.fields) { - // Ignorerte felttyper if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) { continue; } - // Wrapper for hele feltet (Label + Input + Description) 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); - // Lagre referanse til wrapperen basert på Felt-ID fieldWrappers.put(String.valueOf(field.id), fieldWrapper); - // 1. Seksjoner (Overskrifter) if ("section".equals(field.type)) { addSectionHeader(fieldWrapper, field.label, field.description); formContainer.addView(fieldWrapper); continue; } - // 2. HTML-blokker if ("html".equals(field.type)) { if (field.content != null && !field.content.isEmpty()) { TextView htmlView = new TextView(getContext()); @@ -193,7 +250,6 @@ public class FormsFragment extends Fragment { continue; } - // 3. Label (Overskrift for feltet) TextView label = new TextView(getContext()); String labelText = field.label; if (field.isRequired) labelText += " *"; @@ -203,32 +259,38 @@ public class FormsFragment extends Fragment { label.setPadding(0, 10, 0, 5); fieldWrapper.addView(label); - // 4. Input-felt basert på type - if (field.inputs != null && !field.inputs.isEmpty()) { - // Sammensatte felt (Navn, Adresse, Checkbox med inputs) + 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); - } else if ("checkbox".equals(field.type)) { - renderCheckboxField(fieldWrapper, field); + 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); + renderCompositeField(fieldWrapper, field, inputViews, requiredFieldsMap); } } else if ("radio".equals(field.type)) { - renderRadioField(fieldWrapper, field); + renderRadioField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("select".equals(field.type)) { - renderSelectField(fieldWrapper, field); + renderSelectField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("textarea".equals(field.type)) { - renderTextAreaField(fieldWrapper, field); + renderTextAreaField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("date".equals(field.type)) { - renderDateField(fieldWrapper, field); + renderDateField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("consent".equals(field.type)) { - renderConsentField(fieldWrapper, field); + renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap); } else { - // Standard tekst, e-post, telefon, nummer - renderTextField(fieldWrapper, field); + renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap); } - // 5. Beskrivelse (hvis finnes) if (field.description != null && !field.description.isEmpty()) { TextView desc = new TextView(getContext()); desc.setText(Html.fromHtml(field.description, Html.FROM_HTML_MODE_COMPACT)); @@ -240,7 +302,6 @@ public class FormsFragment extends Fragment { formContainer.addView(fieldWrapper); } - // SEND-KNAPP Button dynamicSubmit = new Button(getContext()); dynamicSubmit.setText("Send inn skjema"); dynamicSubmit.setTextColor(Color.WHITE); @@ -251,13 +312,339 @@ public class FormsFragment extends Fragment { dynamicSubmit.setOnClickListener(v -> submitDynamicForm()); formContainer.addView(dynamicSubmit); - // Etter at alt er tegnet, kjør en initial sjekk på logikken evaluateAllConditionalLogic(); } - // --- RENDER METHODS --- + // --- NESTED FORM LOGIKK --- - private void renderTextField(LinearLayout container, GravityField field) { + 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()); + builder.setTitle(childForm.title); + + 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()) { + try { + JSONObject json = new JSONObject(response.body().toString()); + if (json.optBoolean("is_valid")) { + String entryId = json.getString("entry_id"); + String desc = getInputValueGeneric(childInputViews.get("3")); + String price = getInputValueGeneric(childInputViews.get("4")); + addNestedEntry(entryId, desc, price); + dialog.dismiss(); + } + } catch (JSONException e) { e.printStackTrace(); } + } 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); @@ -270,79 +657,39 @@ public class FormsFragment extends Fragment { input.setInputType(InputType.TYPE_CLASS_TEXT); } - UserManager user = UserManager.getInstance(); - String lowerLabel = field.label.toLowerCase(); - - // Autofyll Logikk - if (formId == ID_ANSATTEOPPLYSNINGER) { - if (lowerLabel.contains("e-post") && lowerLabel.contains("arbeid")) { - input.setText(user.getUserEmail()); - } else if (lowerLabel.contains("stilling")) { - input.setText(user.getStilling()); - } else if ((lowerLabel.contains("mobil") || lowerLabel.equals("mobiltelefon"))) { - input.setText(user.getMobiltelefon()); - } - } - else if (formId == ID_HMS_BEKREFTELSE) { - if (lowerLabel.contains("e-post")) { - input.setText(user.getUserEmail()); - } - } - else if (formId == ID_EGENMELDING) { - if (lowerLabel.contains("navn")) { - input.setText(user.getUserDisplayName()); - } else if (lowerLabel.contains("stilling")) { - input.setText(user.getStilling()); - } else if (lowerLabel.contains("e-post")) { - input.setText(user.getUserEmail()); - } + 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()); } - // Lytter for å trigge Conditional Logic når tekst endres input.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} - @Override - public void afterTextChanged(Editable s) { - evaluateAllConditionalLogic(); - } + public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); } }); container.addView(input); - inputViews.put(field.id, input); - requiredFieldsMap.put(field.id, field.isRequired); + views.put(field.id, input); + req.put(field.id, field.isRequired); } - private void renderTextAreaField(LinearLayout container, GravityField field) { + private void renderTextAreaField(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); 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); - - input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - @Override - public void afterTextChanged(Editable s) { - evaluateAllConditionalLogic(); - } - }); - container.addView(input); - inputViews.put(field.id, input); - requiredFieldsMap.put(field.id, field.isRequired); + views.put(field.id, input); + req.put(field.id, field.isRequired); } - private void renderRadioField(LinearLayout container, GravityField field) { + private void renderRadioField(LinearLayout container, GravityField field, Map views, Map req) { RadioGroup group = new RadioGroup(getContext()); - group.setOrientation(RadioGroup.VERTICAL); - if (field.choices != null) { for (GravityField.Choice choice : field.choices) { RadioButton rb = new RadioButton(getContext()); @@ -351,115 +698,87 @@ public class FormsFragment extends Fragment { group.addView(rb); } } - - // Lytter for Conditional Logic - group.setOnCheckedChangeListener((group1, checkedId) -> evaluateAllConditionalLogic()); - + group.setOnCheckedChangeListener((g, i) -> evaluateAllConditionalLogic()); container.addView(group); - inputViews.put(field.id, group); - requiredFieldsMap.put(field.id, field.isRequired); + views.put(field.id, group); + req.put(field.id, field.isRequired); } - private void renderSelectField(LinearLayout container, GravityField field) { + 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); - } + 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); - inputViews.put(field.id, spinner); - requiredFieldsMap.put(field.id, field.isRequired); + views.put(field.id, spinner); + req.put(field.id, field.isRequired); } - private void renderConsentField(LinearLayout container, GravityField field) { + 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; + String cbText = (field.checkboxLabel != null && !field.checkboxLabel.isEmpty()) ? field.checkboxLabel : field.label; checkBox.setText(cbText); - checkBox.setTextSize(16); - checkBox.setPadding(10, 10, 10, 10); - - String inputId = field.id; - if (field.inputs != null && !field.inputs.isEmpty()) { - inputId = field.inputs.get(0).id; - } - - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> evaluateAllConditionalLogic()); - + String inputId = (field.inputs != null && !field.inputs.isEmpty()) ? field.inputs.get(0).id : field.id; + checkBox.setOnCheckedChangeListener((b, c) -> evaluateAllConditionalLogic()); container.addView(checkBox); - inputViews.put(inputId, checkBox); - requiredFieldsMap.put(inputId, field.isRequired); + views.put(inputId, checkBox); + req.put(inputId, field.isRequired); } - private void renderCheckboxField(LinearLayout container, GravityField field) { - // I Gravity Forms er checkboxes en liste av inputs hvis det er flere valg. - // Hvis det bare er ett valg (som "Jeg bekrefter..."), er det ofte én input. - - // Sjekk om det er inputs + private void renderCheckboxField(LinearLayout container, GravityField field, Map views, Map req) { if (field.inputs != null) { for (GravityField inputDef : field.inputs) { CheckBox checkBox = new CheckBox(getContext()); checkBox.setText(inputDef.label); - checkBox.setTextSize(16); - checkBox.setPadding(10, 10, 10, 10); - - // Verdien som sendes er vanligvis etiketten hvis den er huket av. - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> evaluateAllConditionalLogic()); - + checkBox.setOnCheckedChangeListener((b, c) -> evaluateAllConditionalLogic()); container.addView(checkBox); - inputViews.put(inputDef.id, checkBox); - // Checkboxer er sjeldent individuelt påkrevd i en gruppe, men selve feltet er. - // Vi markerer den spesifikke boksen som påkrevd hvis feltet er det og det bare er én. - if (field.inputs.size() == 1) { - requiredFieldsMap.put(inputDef.id, field.isRequired); - } else { - // Hvis det er flere, er validering mer komplekst (minst én må velges). - // Foreløpig enkel implementasjon. - requiredFieldsMap.put(inputDef.id, false); - } + views.put(inputDef.id, checkBox); + req.put(inputDef.id, false); } } } - private void renderDateField(LinearLayout container, GravityField field) { + 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); - dateInput.setFocusable(false); // Hindre tastatur - dateInput.setClickable(true); // Tillat klikk - dateInput.setHint("dd.mm.yyyy"); - - dateInput.setOnClickListener(v -> { - final Calendar c = Calendar.getInstance(); - int year = c.get(Calendar.YEAR); - int month = c.get(Calendar.MONTH); - int day = c.get(Calendar.DAY_OF_MONTH); - - DatePickerDialog datePickerDialog = new DatePickerDialog(getContext(), - (view, year1, monthOfYear, dayOfMonth) -> { - // Format: dd.MM.yyyy - String selectedDate = String.format(java.util.Locale.getDefault(), "%02d.%02d.%d", dayOfMonth, monthOfYear + 1, year1); - dateInput.setText(selectedDate); - evaluateAllConditionalLogic(); - }, year, month, day); - datePickerDialog.show(); - }); container.addView(dateInput); - inputViews.put(field.id, dateInput); - requiredFieldsMap.put(field.id, field.isRequired); + views.put(field.id, dateInput); + req.put(field.id, field.isRequired); } - private void renderCompositeField(LinearLayout container, GravityField parentField) { + private void renderCompositeField(LinearLayout container, GravityField parentField, Map views, Map req) { UserManager user = UserManager.getInstance(); boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER); @@ -497,8 +816,8 @@ public class FormsFragment extends Fragment { } container.addView(subInput); - inputViews.put(subField.id, subInput); - requiredFieldsMap.put(subField.id, isSubRequired); + views.put(subField.id, subInput); + req.put(subField.id, isSubRequired); } } @@ -537,8 +856,6 @@ public class FormsFragment extends Fragment { return 99; } - // --- CONDITIONAL LOGIC ENGINE --- - private void evaluateAllConditionalLogic() { if (currentForm == null || currentForm.fields == null) return; @@ -581,48 +898,36 @@ public class FormsFragment extends Fragment { 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; + 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); - if (view == null) return ""; + return getInputValueGeneric(view); + } - if (view instanceof EditText) { - return ((EditText) view).getText().toString(); - } else if (view instanceof RadioGroup) { - RadioGroup group = (RadioGroup) view; - int checkedId = group.getCheckedRadioButtonId(); - if (checkedId != -1) { - View rb = group.findViewById(checkedId); - if (rb != null && rb.getTag() != null) { - return rb.getTag().toString(); - } + 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(); } - } else if (view instanceof Spinner) { - Spinner spinner = (Spinner) view; - Object item = spinner.getSelectedItem(); - return item != null ? item.toString() : ""; - } else if (view instanceof CheckBox) { - CheckBox cb = (CheckBox) view; - // Hvis sjekket: returner teksten på boksen eller "1" (for consent) - // For standard checkbox i GF brukes ofte verdien som er valgt. - // Hvis det er en enkel avkryssing (type "Bekreftelse"), er "Ja" eller "1" vanlig. - // Vi returnerer teksten hvis den er sjekket, ellers tom streng. - return cb.isChecked() ? cb.getText().toString() : ""; } + if (view instanceof Spinner) { + if (((Spinner) view).getSelectedItemPosition() == 0) return ""; + Object item = ((Spinner) view).getSelectedItem(); + return item != null ? item.toString() : ""; + } + if (view instanceof CheckBox) return ((CheckBox) view).isChecked() ? "1" : ""; return ""; } @@ -633,7 +938,169 @@ public class FormsFragment extends Fragment { } } - // --- DATA HANDLING --- + // --- 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 { + String errBody = response.body().string(); + Log.e(TAG, "Server error body: " + errBody); + // Prøv å vise feilmeldingen fra serveren + updateStatus("Feil (" + response.code() + "): Sjekk logg."); + } 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(); @@ -650,12 +1117,9 @@ public class FormsFragment extends Fragment { String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch; - Request request = new Request.Builder() - .url(url) - .header("Cookie", cookie) - .build(); + Request request = new Request.Builder().url(url).header("Cookie", cookie).build(); - client.newCall(request).enqueue(new Callback() { + 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); @@ -672,8 +1136,6 @@ public class FormsFragment extends Fragment { if (getActivity() != null) { getActivity().runOnUiThread(() -> { showHistory(entries); - - // VIKTIG: AUTO-FYLL SKJER KUN FOR SKJEMA ID 1 ("Ansatteopplysninger") if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0) { try { prefillFormFromHistory(entries.getJSONObject(0)); @@ -688,138 +1150,6 @@ public class FormsFragment extends Fragment { }); } - 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) { - // For Consent er value "1" = Checked. - // For vanlig checkbox kan det være verdien av valget. - 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 submitDynamicForm() { - JSONObject inputValues = new JSONObject(); - boolean hasValues = false; - - for (Map.Entry entry : inputViews.entrySet()) { - String fieldId = entry.getKey(); - View view = entry.getValue(); - - // Hopp over hvis feltet er skjult av Conditional Logic - View wrapper = fieldWrappers.get(fieldId); - // Hvis wrapper er null (f.eks sub-input), sjekk parent wrapper - if (wrapper == null) { - // Forenklet sjekk: hvis selve viewet ikke vises på skjermen - if (!view.isShown()) continue; - } else { - if (wrapper.getVisibility() != View.VISIBLE) continue; - } - - String value = getInputValue(fieldId); - - // Validering - Boolean isRequired = requiredFieldsMap.get(fieldId); - if (isRequired != null && isRequired && value.isEmpty()) { - if (view instanceof EditText) { - ((EditText) view).setError("Må fylles ut"); - view.requestFocus(); - } else if (view instanceof CheckBox) { - Toast.makeText(getContext(), "Du må bekrefte " + ((CheckBox)view).getText(), Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), "Vennligst fyll ut alle obligatoriske felt.", Toast.LENGTH_SHORT).show(); - } - return; - } - - if (!value.isEmpty()) { - try { - inputValues.put("input_" + fieldId, value); - hasValues = true; - } catch (JSONException e) { e.printStackTrace(); } - } - } - - if (!hasValues) { - Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show(); - return; - } - - updateStatus("Sender inn..."); - MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - RequestBody body = RequestBody.create(JSON, inputValues.toString()); - String url = BASE_URL_GF + "/forms/" + formId + "/submissions"; - String cookie = UserManager.getInstance().getCookie(); - - if (cookie == null) { - updateStatus("Du er ikke logget inn."); - return; - } - - Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build(); - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(@NonNull Call call, @NonNull IOException e) { - updateStatus("Nettverksfeil: " + e.getMessage()); - } - - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { - if (response.isSuccessful()) { - if (getActivity() != null) { - getActivity().runOnUiThread(() -> { - Toast.makeText(getContext(), "Skjema sendt!", Toast.LENGTH_LONG).show(); - fetchFormEntries(); - updateStatus("Innsending OK"); - if (formId != ID_ANSATTEOPPLYSNINGER) { - clearInputs(); - } - }); - } - } else { - updateStatus("Feil fra server: " + response.code()); - } - } - }); - } - - private void clearInputs() { - for (View view : inputViews.values()) { - if (view instanceof EditText) ((EditText) view).setText(""); - if (view instanceof RadioGroup) ((RadioGroup) view).clearCheck(); - if (view instanceof Spinner) ((Spinner) view).setSelection(0); - if (view instanceof CheckBox) ((CheckBox) view).setChecked(false); - } - } - private void showHistory(JSONArray entries) { if (historyContainer == null) return; historyContainer.removeAllViews(); @@ -851,6 +1181,62 @@ public class FormsFragment extends Fragment { } 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(""); + if (view instanceof CheckBox) ((CheckBox) view).setChecked(false); + if (view instanceof Button) ((TextView) view.getTag()).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)); diff --git a/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java index e5a61e2..a087cdc 100644 --- a/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java @@ -2,7 +2,6 @@ package com.kbs.kbsintranett; import android.graphics.Color; import android.os.Bundle; -import android.text.Html; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +35,11 @@ public class FormsListFragment extends Fragment { 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) { @@ -71,8 +75,7 @@ public class FormsListFragment extends Fragment { if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE); if (txtStatus != null) txtStatus.setText(""); - Log.d(TAG, "Starter henting av skjemaer..."); - + // Vi bruker standard-kallet som er raskt og effektivt RetrofitClient.getApiService().getFormsListMap().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -81,7 +84,6 @@ public class FormsListFragment extends Fragment { if (response.isSuccessful() && response.body() != null) { Map formMap = response.body(); - Log.d(TAG, "Fant " + formMap.size() + " skjemaer totalt."); if (formMap.isEmpty()) { if (txtStatus != null) txtStatus.setText("Ingen skjemaer funnet."); @@ -90,11 +92,11 @@ public class FormsListFragment extends Fragment { List visibleForms = new ArrayList<>(); for (GravityForm form : formMap.values()) { + // Sjekk om aktiv boolean isActive = form.isActive == null || !"0".equals(form.isActive); - Integer sortOrder = getSortOrderFromDescription(form); - // Logg hva vi finner for hvert skjema - Log.d(TAG, "Skjema: " + form.title + " | ID: " + form.id + " | Beskrivelse: " + form.description + " | Sorteringstall funnet: " + sortOrder); + // 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); @@ -102,16 +104,15 @@ public class FormsListFragment extends Fragment { } if (visibleForms.isEmpty()) { - Log.w(TAG, "Alle skjemaer ble filtrert bort. Sjekk at beskrivelsene starter med et tall."); - if (txtStatus != null) txtStatus.setText("Ingen tilgjengelige skjemaer i appen."); + if (txtStatus != null) txtStatus.setText("Ingen tilgjengelige skjemaer (sjekk nummerering i titler)."); return; } - // Sorter basert på tallet vi fant + // Sorter listen basert på tallet i tittelen Collections.sort(visibleForms, new Comparator() { @Override public int compare(GravityForm f1, GravityForm f2) { - return Integer.compare(getSortOrderFromDescription(f1), getSortOrderFromDescription(f2)); + return Integer.compare(getSortOrderFromTitle(f1.title), getSortOrderFromTitle(f2.title)); } }); @@ -120,7 +121,6 @@ public class FormsListFragment extends Fragment { } } else { - Log.e(TAG, "Feil fra server: " + response.code()); if (txtStatus != null) txtStatus.setText("Kunne ikke laste listen (Kode " + response.code() + ")"); } } @@ -129,29 +129,18 @@ public class FormsListFragment extends Fragment { public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); - Log.e(TAG, "Nettverksfeil", t); if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage()); } }); } /** - * Leter etter et tall i starten av beskrivelsen. - * Håndterer HTML-tags og mellomrom. + * Henter ut tallet fra starten av tittelen. + * Returnerer null hvis tittelen ikke starter med et tall. */ - private Integer getSortOrderFromDescription(GravityForm form) { - if (form.description == null || form.description.isEmpty()) { - return null; // Skjules - } - - // 1. Fjern HTML-tags for å få ren tekst (f.eks "

10.

" -> "10.") - String cleanText = Html.fromHtml(form.description, Html.FROM_HTML_MODE_COMPACT).toString().trim(); - - // 2. Regex: Finn første sekvens av tall (\d+) - // ^\D* betyr "Start på linjen, ignorer ikke-tall (som bokstaver/tegn) i starten" - Pattern p = Pattern.compile("^\\D*(\\d+)"); - Matcher m = p.matcher(cleanText); - + 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)); @@ -162,9 +151,27 @@ public class FormsListFragment extends Fragment { 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()); - button.setText(form.title.toUpperCase()); + + // 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); diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityField.java b/app/src/main/java/com/kbs/kbsintranett/GravityField.java index ae9e738..59e91ee 100644 --- a/app/src/main/java/com/kbs/kbsintranett/GravityField.java +++ b/app/src/main/java/com/kbs/kbsintranett/GravityField.java @@ -11,13 +11,14 @@ public class GravityField { @SerializedName("type") public String type; + @SerializedName("inputType") + public String inputType; + @SerializedName("label") public String label; - // --- DETTE ER FELTET SOM MANGLER --- @SerializedName("adminLabel") public String adminLabel; - // ----------------------------------- @SerializedName("description") public String description; @@ -47,14 +48,20 @@ public class GravityField { @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; - // For å lese Populate Anything-regler @SerializedName("gppa-values-templates") public java.util.Map gppaTemplates; + @SerializedName("gpnfForm") + public String gpnfForm; + public static class Choice { @SerializedName("text") public String text; @@ -65,10 +72,10 @@ public class GravityField { public static class ConditionalLogic { @SerializedName("actionType") - public String actionType; // "show" eller "hide" + public String actionType; @SerializedName("logicType") - public String logicType; // "all" eller "any" + public String logicType; @SerializedName("rules") public List rules; @@ -79,7 +86,7 @@ public class GravityField { public String fieldId; @SerializedName("operator") - public String operator; // "is", "isnot", etc. + public String operator; @SerializedName("value") public String value; diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index a924312..13a87c7 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -437,83 +437,156 @@ 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 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.Comparator; +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.Callback; 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"; - // ID 1 = Ansatteopplysninger (Skal ha autofyll fra historikk) + // 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; // Overskriften "Tidligere innsendinger" + private TextView lblHistory; private ProgressBar loadingSpinner; - private Map dynamicFields = new HashMap<>(); + // --- 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); - // Fallback hvis XML feiler if (formContainer == null) { - // Hvis brukeren ikke har oppdatert XML ennå, unngå krasj formContainer = new LinearLayout(getContext()); } @@ -536,11 +609,11 @@ public class FormsFragment extends Fragment { @Override public void onResponse(retrofit2.Call call, retrofit2.Response response) { if (response.isSuccessful() && response.body() != null) { - GravityForm form = response.body(); + currentForm = response.body(); if (getActivity() != null) { getActivity().runOnUiThread(() -> { if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); - renderDynamicForm(form); + renderDynamicForm(currentForm); fetchFormEntries(); }); } @@ -561,13 +634,15 @@ public class FormsFragment extends Fragment { private void renderDynamicForm(GravityForm form) { if (formContainer == null) return; formContainer.removeAllViews(); - dynamicFields.clear(); + fieldWrappers.clear(); + inputViews.clear(); requiredFieldsMap.clear(); + fileUploads.clear(); + nestedEntries.clear(); updateStatus(""); - // Tittel TextView title = new TextView(getContext()); - title.setText(form.title); + title.setText(getCleanTitle(form.title)); title.setTextSize(24); title.setTypeface(null, Typeface.BOLD); title.setTextColor(Color.BLACK); @@ -576,158 +651,586 @@ public class FormsFragment extends Fragment { if (form.description != null && !form.description.isEmpty()) { TextView formDesc = new TextView(getContext()); - formDesc.setText(form.description); + String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", ""); + formDesc.setText(cleanDesc); formDesc.setPadding(0, 0, 0, 40); formContainer.addView(formDesc); } if (form.fields == null) return; - UserManager user = UserManager.getInstance(); - boolean isPersonaliaSection = true; - - boolean hasFilledName = false; - boolean hasFilledEmail = false; - boolean hasFilledPhone = false; - boolean hasFilledJob = false; - for (GravityField field : form.fields) { + if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) { + continue; + } - if ("hidden".equals(field.type)) continue; - if (field.isHidden) continue; - if ("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)) { - String labelLower = field.label.toLowerCase(); - isPersonaliaSection = labelLower.contains("personalia"); - addSectionHeader(field.label, field.description); + addSectionHeader(fieldWrapper, field.label, field.description); + formContainer.addView(fieldWrapper); continue; } if ("html".equals(field.type)) { - if (field.content != null && field.content.toLowerCase().contains("pårørende")) { - isPersonaliaSection = false; - } if (field.content != null && !field.content.isEmpty()) { TextView htmlView = new TextView(getContext()); htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT)); - htmlView.setPadding(0, 40, 0, 10); - formContainer.addView(htmlView); + fieldWrapper.addView(htmlView); } + formContainer.addView(fieldWrapper); continue; } - if (field.inputs != null && !field.inputs.isEmpty()) { - renderCompositeField(field, isPersonaliaSection); - continue; - } - - // ENKELTFELT TextView label = new TextView(getContext()); String labelText = field.label; if (field.isRequired) labelText += " *"; label.setText(labelText); label.setTextColor(Color.DKGRAY); - label.setPadding(0, 25, 0, 5); - formContainer.addView(label); + label.setTypeface(null, Typeface.BOLD); + label.setPadding(0, 10, 0, 5); + fieldWrapper.addView(label); - EditText input = new EditText(getContext()); - input.setPadding(30, 30, 30, 30); - input.setBackgroundResource(android.R.drawable.edit_text); - - // Autofyll fra UserManager (Standardinfo som alltid er greit å fylle ut) - if (isPersonaliaSection) { - String lowerLabel = field.label.toLowerCase(); - - if (!hasFilledName && (lowerLabel.equals("navn") || lowerLabel.equals("navn *"))) { - input.setText(user.getUserDisplayName()); - hasFilledName = true; - } - else if (!hasFilledEmail && lowerLabel.contains("e-post") && lowerLabel.contains("arbeid")) { - input.setText(user.getUserEmail()); - hasFilledEmail = true; - } - else if (!hasFilledJob && lowerLabel.contains("stilling")) { - input.setText(user.getStilling()); - hasFilledJob = true; - } - else if (!hasFilledPhone && (lowerLabel.contains("mobil") || lowerLabel.equals("mobiltelefon"))) { - input.setText(user.getMobiltelefon()); - hasFilledPhone = true; - } + if ("form".equals(field.type)) { + renderNestedFormField(fieldWrapper, field); } - - 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 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 { - input.setInputType(InputType.TYPE_CLASS_TEXT); + renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap); } - formContainer.addView(input); - if (field.description != null && !field.description.isEmpty()) { - addDescription(field.description); + 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); } - dynamicFields.put(String.valueOf(field.id), input); - requiredFieldsMap.put(String.valueOf(field.id), field.isRequired); + formContainer.addView(fieldWrapper); } - // SEND-KNAPP Button dynamicSubmit = new Button(getContext()); dynamicSubmit.setText("Send inn skjema"); dynamicSubmit.setTextColor(Color.WHITE); dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå - - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); + 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(); } - private void renderCompositeField(GravityField parentField, boolean isPersonaliaSection) { - TextView groupLabel = new TextView(getContext()); - String groupText = parentField.label; - if (parentField.isRequired) groupText += " *"; - groupLabel.setText(groupText); - groupLabel.setTextColor(Color.DKGRAY); - groupLabel.setTypeface(null, Typeface.BOLD); - groupLabel.setPadding(0, 30, 0, 5); - formContainer.addView(groupLabel); + // --- NESTED FORM LOGIKK --- - UserManager user = UserManager.getInstance(); - String lowerParentLabel = parentField.label.toLowerCase(); + private void renderNestedFormField(LinearLayout container, GravityField field) { + nestedEntriesContainer = new LinearLayout(getContext()); + nestedEntriesContainer.setOrientation(LinearLayout.VERTICAL); + nestedEntriesContainer.setPadding(0, 10, 0, 10); + container.addView(nestedEntriesContainer); - List inputs = new ArrayList<>(parentField.inputs); + 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); - // Sortering for adresse - if (parentField.type.equals("address")) { - Collections.sort(inputs, new Comparator() { - @Override - public int compare(GravityField f1, GravityField f2) { - int s1 = getAddressScore(f1.label); - int s2 = getAddressScore(f2.label); - return Integer.compare(s1, s2); + 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()); + builder.setTitle(childForm.title); + + 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()) { + try { + JSONObject json = new JSONObject(response.body().toString()); + if (json.optBoolean("is_valid")) { + String entryId = json.getString("entry_id"); + String desc = getInputValueGeneric(childInputViews.get("3")); + String price = getInputValueGeneric(childInputViews.get("4")); + addNestedEntry(entryId, desc, price); + dialog.dismiss(); + } + } catch (JSONException e) { e.printStackTrace(); } + } 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; + 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 (GravityField inputDef : field.inputs) { + CheckBox checkBox = new CheckBox(getContext()); + checkBox.setText(inputDef.label); + 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) continue; - if ("hidden".equals(subField.visibility)) continue; + if (subField.isHidden || "hidden".equals(subField.visibility)) continue; TextView subLabel = new TextView(getContext()); String subLabelText = subField.label; - boolean isSubRequired = parentField.isRequired; - if (parentField.type.equals("address") && subField.id.endsWith(".2")) { + if ("address".equals(parentField.type) && subField.id.endsWith(".2")) { isSubRequired = false; } if (isSubRequired) subLabelText += " *"; @@ -736,32 +1239,47 @@ public class FormsFragment extends Fragment { subLabel.setTextColor(Color.GRAY); subLabel.setTextSize(12); subLabel.setPadding(0, 10, 0, 0); - formContainer.addView(subLabel); + 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 (isPersonaliaSection && lowerParentLabel.contains("navn") && !lowerParentLabel.contains("pårørende")) { - String lowerSubLabel = subField.label.toLowerCase(); - if (lowerSubLabel.contains("fornavn")) { - subInput.setText(user.getFirstName()); - } - else if (lowerSubLabel.contains("etternavn")) { - subInput.setText(user.getLastName()); - } + 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()); } - formContainer.addView(subInput); + container.addView(subInput); + views.put(subField.id, subInput); + req.put(subField.id, isSubRequired); + } + } - dynamicFields.put(subField.id, subInput); - requiredFieldsMap.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); } - if (parentField.description != null && !parentField.description.isEmpty()) { - addDescription(parentField.description); - } + 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) { @@ -775,32 +1293,250 @@ public class FormsFragment extends Fragment { return 99; } - private void addDescription(String text) { - TextView desc = new TextView(getContext()); - desc.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)); - desc.setTextSize(12); - desc.setTextColor(Color.GRAY); - desc.setPadding(5, 5, 5, 0); - formContainer.addView(desc); + private void 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 void addSectionHeader(String title, String descText) { - TextView sectionHeader = new TextView(getContext()); - sectionHeader.setText(title); - sectionHeader.setTextSize(18); - sectionHeader.setTypeface(null, Typeface.BOLD); - sectionHeader.setTextColor(Color.parseColor("#0069B3")); - sectionHeader.setPadding(0, 50, 0, 5); - formContainer.addView(sectionHeader); + private boolean evaluateLogic(GravityField.ConditionalLogic logic) { + if (logic.rules == null || logic.rules.isEmpty()) return true; - if (descText != null && !descText.isEmpty()) { - addDescription(descText); + boolean isAll = "all".equalsIgnoreCase(logic.logicType); + boolean aggregatedResult = isAll; + + for (GravityField.Rule rule : logic.rules) { + String val = getInputValue(rule.fieldId); + boolean ruleMatch = checkRule(val, rule.operator, rule.value); + + if (isAll) { + aggregatedResult = aggregatedResult && ruleMatch; + if (!aggregatedResult) break; + } else { + aggregatedResult = aggregatedResult || ruleMatch; + if (aggregatedResult) break; + } + } + return aggregatedResult; + } + + private boolean checkRule(String actualValue, String operator, String targetValue) { + if (actualValue == null) actualValue = ""; + if (targetValue == null) targetValue = ""; + + switch (operator.toLowerCase()) { + case "is": return actualValue.equalsIgnoreCase(targetValue); + case "isnot": return !actualValue.equalsIgnoreCase(targetValue); + case "contains": return actualValue.toLowerCase().contains(targetValue.toLowerCase()); + case "starts_with": return actualValue.toLowerCase().startsWith(targetValue.toLowerCase()); + case "ends_with": return actualValue.toLowerCase().endsWith(targetValue.toLowerCase()); + default: return false; + } + } + + private String getInputValue(String fieldId) { + View view = inputViews.get(fieldId); + return getInputValueGeneric(view); + } + + private String getInputValueGeneric(View view) { + if (view == null) return ""; + if (view instanceof EditText) return ((EditText) view).getText().toString(); + if (view instanceof RadioGroup) { + int id = ((RadioGroup) view).getCheckedRadioButtonId(); + if (id != -1) { + View rb = view.findViewById(id); + if (rb != null && rb.getTag() != null) return rb.getTag().toString(); + } + } + if (view instanceof Spinner) { + if (((Spinner) view).getSelectedItemPosition() == 0) return ""; + Object item = ((Spinner) view).getSelectedItem(); + return item != null ? item.toString() : ""; + } + if (view instanceof CheckBox) return ((CheckBox) view).isChecked() ? "1" : ""; + 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) {} + } } - View line = new View(getContext()); - line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2)); - line.setBackgroundColor(Color.LTGRAY); - formContainer.addView(line); + 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 { + String errBody = response.body().string(); + Log.e(TAG, "Server error body: " + errBody); + // Prøv å vise feilmeldingen fra serveren + updateStatus("Feil (" + response.code() + "): Sjekk logg."); + } 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() { @@ -818,12 +1554,9 @@ public class FormsFragment extends Fragment { String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch; - Request request = new Request.Builder() - .url(url) - .header("Cookie", cookie) - .build(); + Request request = new Request.Builder().url(url).header("Cookie", cookie).build(); - client.newCall(request).enqueue(new Callback() { + 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); @@ -840,9 +1573,7 @@ public class FormsFragment extends Fragment { if (getActivity() != null) { getActivity().runOnUiThread(() -> { showHistory(entries); - - // VIKTIG: AUTO-FYLL SKJER NÅ KUN FOR SKJEMA ID 1 - if (entries.length() > 0 && formId == ID_ANSATTEOPPLYSNINGER) { + if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0) { try { prefillFormFromHistory(entries.getJSONObject(0)); } catch (JSONException e) { e.printStackTrace(); } @@ -856,94 +1587,11 @@ public class FormsFragment extends Fragment { }); } - private void prefillFormFromHistory(JSONObject latestEntry) { - for (Map.Entry mapEntry : dynamicFields.entrySet()) { - String fieldId = mapEntry.getKey(); - EditText input = mapEntry.getValue(); - - if (input.getText().length() > 0) continue; - - if (latestEntry.has(fieldId)) { - String prevValue = latestEntry.optString(fieldId); - if (!prevValue.isEmpty()) { - input.setText(prevValue); - } - } - } - updateStatus("Skjemaet er forhåndsutfylt fra din forrige innsending."); - } - - private void submitDynamicForm() { - JSONObject inputValues = new JSONObject(); - boolean hasValues = false; - - for (Map.Entry entry : dynamicFields.entrySet()) { - String fieldId = entry.getKey(); - EditText input = entry.getValue(); - String value = input.getText().toString().trim(); - - Boolean isRequired = requiredFieldsMap.get(fieldId); - if (isRequired != null && isRequired && value.isEmpty()) { - input.setError("Må fylles ut"); - input.requestFocus(); - return; - } - - if (!value.isEmpty()) { - try { - inputValues.put("input_" + fieldId, value); - hasValues = true; - } catch (JSONException e) { e.printStackTrace(); } - } - } - - if (!hasValues) { - Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show(); - return; - } - - updateStatus("Sender inn..."); - MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - RequestBody body = RequestBody.create(JSON, inputValues.toString()); - String url = BASE_URL_GF + "/forms/" + formId + "/submissions"; - String cookie = UserManager.getInstance().getCookie(); - - if (cookie == null) { - updateStatus("Du er ikke logget inn."); - return; - } - - Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build(); - - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(@NonNull Call call, @NonNull IOException e) { - updateStatus("Nettverksfeil: " + e.getMessage()); - } - - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { - if (response.isSuccessful()) { - if (getActivity() != null) { - getActivity().runOnUiThread(() -> { - Toast.makeText(getContext(), "Skjema sendt!", Toast.LENGTH_LONG).show(); - fetchFormEntries(); - updateStatus("Innsending OK"); - }); - } - } else { - updateStatus("Feil fra server: " + response.code()); - } - } - }); - } - private void showHistory(JSONArray entries) { if (historyContainer == null) return; historyContainer.removeAllViews(); if (entries.length() == 0) { - // Skjul overskriften hvis det ikke er historikk if (lblHistory != null) lblHistory.setVisibility(View.GONE); return; } else { @@ -951,25 +1599,81 @@ public class FormsFragment extends Fragment { } try { - for (int i = 0; i < entries.length(); i++) { + 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(date); + item.setText("Innsendt: " + date); item.setPadding(10, 20, 10, 20); - - // Gjør historikken klikkbar (for fremtidig funksjonalitet) item.setBackgroundResource(android.R.drawable.list_selector_background); View line = new View(getContext()); line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); line.setBackgroundColor(Color.LTGRAY); + historyContainer.addView(item); historyContainer.addView(line); } } catch (JSONException e) { e.printStackTrace(); } } + private void 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(""); + if (view instanceof CheckBox) ((CheckBox) view).setChecked(false); + if (view instanceof Button) ((TextView) view.getTag()).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)); @@ -984,6 +1688,7 @@ 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; @@ -997,8 +1702,13 @@ 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.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; @@ -1006,30 +1716,29 @@ 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) { - // Vi gjenbruker samme layout som detaljvisningen, siden den nå er ryddig View view = inflater.inflate(R.layout.fragment_forms, container, false); - this.container = view.findViewById(R.id.form_container); this.loadingSpinner = view.findViewById(R.id.loading_spinner); this.txtStatus = view.findViewById(R.id.txt_status); - // Skjul "Tidligere innsendinger" tekst og container på denne siden, - // da vi kun skal vise en meny her. - View historyTitle = view.findViewById(R.id.historyContainer); // Egentlig containeren, men vi skjuler alt under + View historyTitle = view.findViewById(R.id.historyContainer); if (historyTitle != null) historyTitle.setVisibility(View.GONE); - // Finn og skjul "Tidligere innsendinger" overskriften hvis mulig - // (Siden vi bruker en delt XML er dette litt "hacky", men funker) LinearLayout mainLayout = view.findViewById(R.id.main_layout); if (mainLayout != null && mainLayout.getChildCount() > 4) { - // Skjuler elementene nederst som hører til historikk for(int i = 0; i < mainLayout.getChildCount(); i++) { View child = mainLayout.getChildAt(i); if (child instanceof TextView && ((TextView)child).getText().toString().contains("Tidligere")) { @@ -1052,13 +1761,11 @@ public class FormsListFragment extends Fragment { if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE); if (txtStatus != null) txtStatus.setText(""); - // Hent listen over skjemaer (Map ID -> Skjema) + // 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; - - // SKJUL SPINNER NÅR VI FÅR SVAR if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { @@ -1069,16 +1776,36 @@ public class FormsListFragment extends Fragment { return; } - // Vi sorterer listen basert på ID (eller tittel) for ryddighet - Map sortedMap = new TreeMap<>(formMap); + List visibleForms = new ArrayList<>(); + for (GravityForm form : formMap.values()) { + // Sjekk om aktiv + boolean isActive = form.isActive == null || !"0".equals(form.isActive); - for (Map.Entry entry : sortedMap.entrySet()) { - GravityForm form = entry.getValue(); - // Sjekk at skjemaet er aktivt og har en tittel - if (form != null && form.title != null) { - addFormButton(form); + // 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() + ")"); } @@ -1087,34 +1814,66 @@ public class FormsListFragment extends Fragment { @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; - // SKJUL SPINNER VED FEIL if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); if (txtStatus != null) txtStatus.setText("Nettverksfeil: " + t.getMessage()); } }); } + /** + * 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()); - button.setText(form.title.toUpperCase()); // Store bokstaver ser ofte ryddigere ut i menyer + + // 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); - // Klikk-lytter button.setOnClickListener(v -> { Bundle bundle = new Bundle(); bundle.putInt("formId", form.id); Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle); }); - // Layout parametere (Marginer) LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ); - params.setMargins(0, 0, 0, 20); // Avstand mellom knappene + params.setMargins(0, 0, 0, 20); button.setLayoutParams(params); container.addView(button); @@ -1174,9 +1933,15 @@ public class GravityField { @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; @@ -1205,14 +1970,20 @@ public class GravityField { @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; - // NYTT: For å lese Populate Anything-regler @SerializedName("gppa-values-templates") public java.util.Map gppaTemplates; + @SerializedName("gpnfForm") + public String gpnfForm; + public static class Choice { @SerializedName("text") public String text; @@ -1223,10 +1994,10 @@ public class GravityField { public static class ConditionalLogic { @SerializedName("actionType") - public String actionType; // "show" eller "hide" + public String actionType; @SerializedName("logicType") - public String logicType; // "all" eller "any" + public String logicType; @SerializedName("rules") public List rules; @@ -1237,7 +2008,7 @@ public class GravityField { public String fieldId; @SerializedName("operator") - public String operator; // "is", "isnot", etc. + public String operator; @SerializedName("value") public String value; @@ -1262,8 +2033,11 @@ public class GravityForm { @SerializedName("description") public String description; + @SerializedName("is_active") + public String isActive; // "1" = Aktiv, "0" = Inaktiv + @SerializedName("fields") - public List fields; // En liste med alle feltene i skjemaet + public List fields; } ============================================================ @@ -1678,7 +2452,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; public class MainActivity extends AppCompatActivity { // VIKTIG: Erstatt denne med din Web Client ID - public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com"; + public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; private static final String TAG = "MainActivity"; private NavController navController; private BottomNavigationView bottomNav;