kbsintranett/app/src/main/java/com/kbs/kbsintranett/FormsFragment.java
2026-01-21 14:14:08 +01:00

920 lines
No EOL
43 KiB
Java

package com.kbs.kbsintranett;
import android.Manifest;
import android.animation.LayoutTransition;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
public class FormsFragment extends Fragment {
private static final String TAG = "FormsFragment";
private static final String BASE_URL_GF = "https://intranet.kbs.no/wp-json/gf/v2";
private static final int ID_ANSATTEOPPLYSNINGER = 1;
private static final int ID_REFUSJON_UTLEGG = 16;
private int formId = 1;
private LinearLayout formContainer;
private LinearLayout historyContainer;
private View historyWrapper;
private TextView txtStatus;
private TextView lblHistory;
private ProgressBar loadingSpinner;
private ImageView btnToggleHistory;
private Map<String, View> fieldWrappers = new HashMap<>();
private Map<String, View> inputViews = new HashMap<>();
private Map<String, Boolean> requiredFieldsMap = new HashMap<>();
private Map<String, Uri> fileUploads = new HashMap<>();
private Map<String, View> childInputViews = new HashMap<>();
private Map<String, Boolean> childRequiredFieldsMap = new HashMap<>();
private Map<String, Uri> childFileUploads = new HashMap<>();
private List<NestedEntry> nestedEntries = new ArrayList<>();
private LinearLayout nestedEntriesContainer;
private TextView totalAmountView;
private String pendingFileFieldId = null;
private boolean isSelectingForChild = false;
private Uri currentPhotoUri = null;
private ActivityResultLauncher<Intent> filePickerLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
private ActivityResultLauncher<String> requestPermissionLauncher;
private GravityForm currentForm;
private final OkHttpClient client = new OkHttpClient();
private static final Pattern TITLE_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)");
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, uri, isSelectingForChild);
}
}
}
);
takePictureLauncher = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
success -> {
if (success && currentPhotoUri != null && pendingFileFieldId != null) {
handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild);
}
}
);
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) openCamera();
else Toast.makeText(getContext(), "Kameratillatelse trengs.", Toast.LENGTH_SHORT).show();
}
);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_forms, container, false);
formContainer = view.findViewById(R.id.form_container);
historyContainer = view.findViewById(R.id.historyContainer);
historyWrapper = view.findViewById(R.id.history_wrapper);
txtStatus = view.findViewById(R.id.txt_status);
lblHistory = view.findViewById(R.id.lbl_history);
loadingSpinner = view.findViewById(R.id.loading_spinner);
btnToggleHistory = view.findViewById(R.id.btn_toggle_history);
if (btnToggleHistory != null) btnToggleHistory.setOnClickListener(v -> toggleHistoryVisibility());
if (getArguments() != null) {
int argId = getArguments().getInt("formId", 0);
if (argId != 0) formId = argId;
}
fetchFormStructure();
return view;
}
private void toggleHistoryVisibility() {
if (historyWrapper == null || btnToggleHistory == null) return;
if (historyWrapper.getVisibility() == View.VISIBLE) {
historyWrapper.setVisibility(View.GONE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float);
} else {
historyWrapper.setVisibility(View.VISIBLE);
btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float);
}
}
private void fetchFormStructure() {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE);
WordPressApiService api = RetrofitClient.getApiService();
api.getForm(formId).enqueue(new retrofit2.Callback<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> response) {
if (response.isSuccessful() && response.body() != null) {
currentForm = response.body();
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
renderDynamicForm(currentForm);
fetchFormEntries();
});
}
}
}
@Override public void onFailure(retrofit2.Call<GravityForm> call, Throwable t) {
if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE);
}
});
}
private void renderDynamicForm(GravityForm form) {
if (formContainer == null) return;
formContainer.removeAllViews();
fieldWrappers.clear();
inputViews.clear();
requiredFieldsMap.clear();
fileUploads.clear();
nestedEntries.clear();
// 1. OPPSETT AV TOOLBAR (MainActivity sin toolbar)
if (getActivity() instanceof AppCompatActivity) {
AppCompatActivity activity = (AppCompatActivity) getActivity();
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle("Fyll ut skjema");
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false); // Fjerner pil tilbake
}
}
// 2. OVERSKRIFT I SKJEMAET
TextView headerTitle = new TextView(getContext());
headerTitle.setText("Fyll ut " + getCleanTitle(form.title));
headerTitle.setTextSize(22); // Rettet fra 22sp til 22
headerTitle.setTypeface(null, Typeface.BOLD);
headerTitle.setTextColor(Color.BLACK);
headerTitle.setPadding(0, 10, 0, 30);
formContainer.addView(headerTitle);
if (form.description != null && !form.description.isEmpty()) {
TextView formDesc = new TextView(getContext());
formDesc.setText(form.description.replaceFirst("^\\d+\\.\\s*", ""));
formDesc.setPadding(0, 0, 0, 40);
formContainer.addView(formDesc);
}
if (form.fields == null) return;
for (GravityField field : form.fields) {
if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) continue;
LinearLayout fieldWrapper = new LinearLayout(getContext());
fieldWrapper.setOrientation(LinearLayout.VERTICAL);
fieldWrapper.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
fieldWrapper.setPadding(0, 10, 0, 20);
fieldWrappers.put(String.valueOf(field.id), fieldWrapper);
if ("section".equals(field.type)) {
addSectionHeader(fieldWrapper, field.label, field.description);
formContainer.addView(fieldWrapper);
continue;
}
if ("html".equals(field.type)) {
if (field.content != null && !field.content.isEmpty()) {
TextView htmlView = new TextView(getContext());
htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT));
fieldWrapper.addView(htmlView);
}
formContainer.addView(fieldWrapper);
continue;
}
TextView label = new TextView(getContext());
String labelText = field.label;
if (field.isRequired) labelText += " *";
label.setText(labelText);
label.setTextColor(Color.DKGRAY);
label.setTypeface(null, Typeface.BOLD);
label.setPadding(0, 10, 0, 5);
fieldWrapper.addView(label);
if ("form".equals(field.type)) renderNestedFormField(fieldWrapper, field);
else if ("product".equals(field.type) && "calculation".equals(field.inputType)) renderTotalSumField(fieldWrapper, field);
else if ("time".equals(field.type)) renderTimeField(fieldWrapper, field, inputViews, requiredFieldsMap);
else if ("fileupload".equals(field.type)) renderFileUploadField(fieldWrapper, field, inputViews, requiredFieldsMap, false);
else if (field.inputs != null && !field.inputs.isEmpty()) {
if ("consent".equals(field.type)) renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap);
else if ("checkbox".equals(field.type) || "multi_choice".equals(field.type)) renderCheckboxField(fieldWrapper, field, inputViews, requiredFieldsMap);
else renderCompositeField(fieldWrapper, field, inputViews, requiredFieldsMap);
} else if ("radio".equals(field.type)) renderRadioField(fieldWrapper, field, inputViews, requiredFieldsMap);
else if ("select".equals(field.type)) renderSelectField(fieldWrapper, field, inputViews, requiredFieldsMap);
else if ("textarea".equals(field.type)) renderTextAreaField(fieldWrapper, field, inputViews, requiredFieldsMap);
else if ("date".equals(field.type)) renderDateField(fieldWrapper, field, inputViews, requiredFieldsMap);
else renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap);
if (field.description != null && !field.description.isEmpty()) {
TextView desc = new TextView(getContext());
desc.setText(Html.fromHtml(field.description, Html.FROM_HTML_MODE_COMPACT));
desc.setTextSize(12);
desc.setTextColor(Color.GRAY);
fieldWrapper.addView(desc);
}
formContainer.addView(fieldWrapper);
}
Button dynamicSubmit = new Button(getContext());
dynamicSubmit.setText("Send inn skjema");
dynamicSubmit.setTextColor(Color.WHITE);
dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3"));
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 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, field.id);
});
container.addView(btnAdd);
EditText hiddenIds = new EditText(getContext());
hiddenIds.setVisibility(View.GONE);
inputViews.put(field.id, hiddenIds);
}
private void renderTotalSumField(LinearLayout container, GravityField field) {
totalAmountView = new TextView(getContext());
totalAmountView.setText("Kr 0,00");
totalAmountView.setTextSize(18);
totalAmountView.setTypeface(null, Typeface.BOLD);
totalAmountView.setPadding(10, 10, 10, 10);
container.addView(totalAmountView);
}
private void openChildFormDialog(int childFormId, String parentFieldId) {
ProgressBar pBar = new ProgressBar(getContext());
AlertDialog loadingDialog = new AlertDialog.Builder(getContext())
.setView(pBar)
.setMessage("Laster skjema...")
.setCancelable(false)
.show();
RetrofitClient.getApiService().getForm(childFormId).enqueue(new retrofit2.Callback<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> response) {
loadingDialog.dismiss();
if (response.isSuccessful() && response.body() != null) {
showChildFormDialog(response.body(), parentFieldId);
}
}
@Override public void onFailure(retrofit2.Call<GravityForm> call, Throwable t) { loadingDialog.dismiss(); }
});
}
private void showChildFormDialog(GravityForm childForm, String parentFieldId) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
childInputViews.clear();
childRequiredFieldsMap.clear();
childFileUploads.clear();
ScrollView scrollView = new ScrollView(getContext());
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(30, 30, 30, 30);
scrollView.addView(layout);
for (GravityField field : childForm.fields) {
if ("hidden".equals(field.type) || field.isHidden) continue;
LinearLayout wrapper = new LinearLayout(getContext());
wrapper.setOrientation(LinearLayout.VERTICAL);
wrapper.setPadding(0, 10, 0, 20);
TextView label = new TextView(getContext());
label.setText(field.label + (field.isRequired ? " *" : ""));
label.setTypeface(null, Typeface.BOLD);
wrapper.addView(label);
if ("fileupload".equals(field.type)) {
renderFileUploadField(wrapper, field, childInputViews, childRequiredFieldsMap, true);
} else if ("product".equals(field.type)) {
EditText input = new EditText(getContext());
input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
input.setHint("Kr 0.00");
wrapper.addView(input);
childInputViews.put(field.id, input);
childRequiredFieldsMap.put(field.id, field.isRequired);
} else {
renderTextField(wrapper, field, childInputViews, childRequiredFieldsMap);
}
layout.addView(wrapper);
}
builder.setView(scrollView);
builder.setPositiveButton("Legg til vedlegg", null);
builder.setNegativeButton("Avbryt", (d, w) -> d.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
submitChildForm(childForm.id, dialog, parentFieldId);
});
}
private void submitChildForm(int childFormId, AlertDialog dialog, String parentFieldId) {
JSONObject inputValues = new JSONObject();
for (Map.Entry<String, View> entry : childInputViews.entrySet()) {
String val = getInputValueGeneric(entry.getValue());
if (!val.isEmpty()) {
try { inputValues.put("input_" + entry.getKey(), val); } catch (JSONException e) { e.printStackTrace(); }
}
}
List<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String key = names.getString(i);
textParts.put(key, RequestBody.create(MultipartBody.FORM, inputValues.getString(key)));
}
}
if (parentFieldId != null) textParts.put("gpnf_entry_nested_form_field", RequestBody.create(MultipartBody.FORM, parentFieldId));
for (Map.Entry<String, Uri> fileEntry : childFileUploads.entrySet()) {
MultipartBody.Part part = getFilePart("input_" + fileEntry.getKey(), fileEntry.getValue());
if (part != null) fileParts.add(part);
}
Toast.makeText(getContext(), "Laster opp...", Toast.LENGTH_SHORT).show();
RetrofitClient.getApiService().submitMultipartForm(childFormId, textParts, fileParts).enqueue(new retrofit2.Callback<JsonElement>() {
@Override
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> response) {
if (response.isSuccessful() && response.body() != null) {
JsonObject json = response.body().getAsJsonObject();
if (json.has("is_valid") && json.get("is_valid").getAsBoolean()) {
String entryId = json.get("entry_id").getAsString();
String desc = getInputValueGeneric(childInputViews.get("3"));
String price = getInputValueGeneric(childInputViews.get("4"));
addNestedEntry(entryId, desc, price);
dialog.dismiss();
}
}
}
@Override public void onFailure(retrofit2.Call<JsonElement> call, Throwable t) {}
});
} catch (Exception e) { e.printStackTrace(); }
}
private void addNestedEntry(String entryId, String description, String price) {
nestedEntries.add(new NestedEntry(entryId, description, price));
refreshNestedList();
}
private void refreshNestedList() {
if (nestedEntriesContainer == null) return;
nestedEntriesContainer.removeAllViews();
double total = 0;
List<String> ids = new ArrayList<>();
for (NestedEntry entry : nestedEntries) {
ids.add(entry.id);
String cleanPrice = entry.price.replaceAll("[^0-9,.]", "").replace(",", ".");
try { if (!cleanPrice.isEmpty()) total += Double.parseDouble(cleanPrice); } catch (NumberFormatException ignored) {}
TextView txt = new TextView(getContext());
txt.setText(entry.description + " (" + entry.price + ")");
txt.setPadding(10, 10, 10, 10);
nestedEntriesContainer.addView(txt);
}
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));
}
private void renderFileUploadField(LinearLayout container, GravityField field, Map<String, View> viewsMap, Map<String, Boolean> reqMap, boolean isChild) {
LinearLayout fileLayout = new LinearLayout(getContext());
fileLayout.setOrientation(LinearLayout.HORIZONTAL);
Button btnUpload = new Button(getContext());
btnUpload.setText("Velg fil / Kamera");
btnUpload.setOnClickListener(v -> {
pendingFileFieldId = field.id;
isSelectingForChild = isChild;
showFileSourceDialog();
});
TextView txtFileName = new TextView(getContext());
txtFileName.setText("Ingen valgt");
txtFileName.setPadding(20, 0, 0, 0);
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 showFileSourceDialog() {
String[] options = {"Ta bilde", "Velg fil"};
new AlertDialog.Builder(getContext()).setTitle("Last opp").setItems(options, (dialog, which) -> {
if (which == 0) {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) openCamera();
else requestPermissionLauncher.launch(Manifest.permission.CAMERA);
} else {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
filePickerLauncher.launch(intent);
}
}).show();
}
private void openCamera() {
currentPhotoUri = createImageUri();
if (currentPhotoUri != null) takePictureLauncher.launch(currentPhotoUri);
}
private Uri createImageUri() {
try {
File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile("IMG_", ".jpg", storageDir);
return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image);
} catch (IOException e) { return null; }
}
private void handleFileSelection(String fieldId, Uri uri, boolean isChild) {
if (isChild) childFileUploads.put(fieldId, uri);
else fileUploads.put(fieldId, uri);
View view = (isChild ? childInputViews : inputViews).get(fieldId);
if (view instanceof Button) {
TextView txtView = (TextView) view.getTag();
if (txtView != null) txtView.setText(getFileName(uri));
}
}
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);
}
}
}
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());
return m.find() ? m.group(2) : title;
}
private void renderTimeField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
EditText timeInput = new EditText(getContext());
timeInput.setFocusable(false);
timeInput.setHint("00:00");
timeInput.setOnClickListener(v -> {
Calendar c = Calendar.getInstance();
new TimePickerDialog(getContext(), (tp, h, m) -> {
timeInput.setText(String.format("%02d:%02d", h, m));
evaluateAllConditionalLogic();
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
});
container.addView(timeInput);
views.put(field.id, timeInput);
req.put(field.id, field.isRequired);
}
private void renderTextField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
EditText input = new EditText(getContext());
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);
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")) input.setText(user.getUserDisplayName());
}
input.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); }
});
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderTextAreaField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
EditText input = new EditText(getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
input.setMinLines(3);
container.addView(input);
views.put(field.id, input);
req.put(field.id, field.isRequired);
}
private void renderRadioField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
RadioGroup group = new RadioGroup(getContext());
if (field.choices != null) {
for (GravityField.Choice choice : field.choices) {
RadioButton rb = new RadioButton(getContext());
rb.setText(choice.text);
rb.setTag(choice.value);
rb.setOnClickListener(v -> evaluateAllConditionalLogic());
group.addView(rb);
}
}
container.addView(group);
views.put(field.id, group);
req.put(field.id, field.isRequired);
}
private void renderSelectField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
Spinner spinner = new Spinner(getContext());
List<String> labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) { for (GravityField.Choice c : field.choices) labels.add(c.text); }
spinner.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, labels));
container.addView(spinner);
views.put(field.id, spinner);
req.put(field.id, field.isRequired);
}
private void renderConsentField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
CheckBox checkBox = new CheckBox(getContext());
checkBox.setText(field.checkboxLabel != null ? field.checkboxLabel : field.label);
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<String, View> views, Map<String, Boolean> req) {
if (field.inputs != null) {
for (GravityField inputDef : field.inputs) {
CheckBox cb = new CheckBox(getContext());
cb.setText(inputDef.label);
cb.setOnCheckedChangeListener((b, c) -> evaluateAllConditionalLogic());
container.addView(cb);
views.put(inputDef.id, cb);
}
}
}
private void renderDateField(LinearLayout container, GravityField field, Map<String, View> views, Map<String, Boolean> req) {
EditText dateInput = new EditText(getContext());
dateInput.setFocusable(false);
dateInput.setHint("dd.mm.yyyy");
dateInput.setOnClickListener(v -> {
Calendar c = Calendar.getInstance();
new DatePickerDialog(getContext(), (view, y, m, d) -> {
dateInput.setText(String.format("%02d.%02d.%d", d, m + 1, y));
evaluateAllConditionalLogic();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
});
container.addView(dateInput);
views.put(field.id, dateInput);
req.put(field.id, field.isRequired);
}
private void renderCompositeField(LinearLayout container, GravityField parentField, Map<String, View> views, Map<String, Boolean> req) {
if (parentField.inputs == null) return;
for (GravityField subField : parentField.inputs) {
TextView subLabel = new TextView(getContext());
subLabel.setText(subField.label + (parentField.isRequired ? " *" : ""));
subLabel.setTextSize(12);
container.addView(subLabel);
EditText subInput = new EditText(getContext());
container.addView(subInput);
views.put(subField.id, subInput);
req.put(subField.id, parentField.isRequired);
}
}
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);
container.addView(desc);
}
View line = new View(getContext());
line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2));
line.setBackgroundColor(Color.LTGRAY);
container.addView(line);
}
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);
setViewVisibility(field.id, (show && isMatch) || (!show && !isMatch));
}
}
private boolean evaluateLogic(GravityField.ConditionalLogic logic) {
if (logic.rules == null || logic.rules.isEmpty()) return true;
boolean isAll = "all".equalsIgnoreCase(logic.logicType);
boolean aggregatedResult = isAll;
for (GravityField.Rule rule : logic.rules) {
String val = getInputValue(rule.fieldId);
boolean ruleMatch = checkRule(val, rule.operator, rule.value);
if (isAll) { aggregatedResult &= ruleMatch; if (!aggregatedResult) break; }
else { aggregatedResult |= ruleMatch; if (aggregatedResult) break; }
}
return aggregatedResult;
}
private boolean checkRule(String actual, String op, String target) {
if (actual == null) actual = ""; if (target == null) target = "";
switch (op.toLowerCase()) {
case "is": return actual.equalsIgnoreCase(target);
case "isnot": return !actual.equalsIgnoreCase(target);
case "contains": return actual.toLowerCase().contains(target.toLowerCase());
default: return false;
}
}
private String getInputValue(String fieldId) { return getInputValueGeneric(inputViews.get(fieldId)); }
private String getInputValueGeneric(View view) {
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) return ((Spinner) view).getSelectedItemPosition() > 0 ? ((Spinner) view).getSelectedItem().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);
}
private void submitDynamicForm() {
JSONObject inputValues = new JSONObject();
boolean hasValues = false;
for (Map.Entry<String, View> entry : inputViews.entrySet()) {
View wrapper = fieldWrappers.get(entry.getKey());
if (wrapper != null && wrapper.getVisibility() != View.VISIBLE) continue;
String val = getInputValueGeneric(entry.getValue());
if (requiredFieldsMap.getOrDefault(entry.getKey(), false) && val.isEmpty() && !(entry.getValue() instanceof Button)) {
Toast.makeText(getContext(), "Fyll ut påkrevde felt", Toast.LENGTH_SHORT).show();
return;
}
if (!val.isEmpty()) {
try { inputValues.put("input_" + entry.getKey(), val); hasValues = true; } catch (JSONException ignored) {}
}
}
if (!hasValues && fileUploads.isEmpty()) return;
updateStatus("Sender inn...");
if (!fileUploads.isEmpty()) sendMultipart(inputValues);
else {
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", UserManager.getInstance().getCookie()).build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override public void onResponse(okhttp3.Call call, Response response) {
if (response.isSuccessful() && getActivity() != null) getActivity().runOnUiThread(() -> { Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); clearInputs(); fetchFormEntries(); });
}
@Override public void onFailure(okhttp3.Call call, IOException e) {}
});
}
}
private void sendMultipart(JSONObject inputValues) {
List<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String k = names.getString(i);
textParts.put(k, RequestBody.create(MultipartBody.FORM, inputValues.getString(k)));
}
}
for (Map.Entry<String, Uri> entry : fileUploads.entrySet()) {
MultipartBody.Part part = getFilePart("input_" + entry.getKey(), entry.getValue());
if (part != null) fileParts.add(part);
}
RetrofitClient.getApiService().submitMultipartForm(formId, textParts, fileParts).enqueue(new retrofit2.Callback<JsonElement>() {
@Override public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> response) {
if (response.isSuccessful() && getActivity() != null) getActivity().runOnUiThread(() -> { Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); clearInputs(); fetchFormEntries(); });
}
@Override public void onFailure(retrofit2.Call<JsonElement> call, Throwable t) {}
});
} catch (Exception ignored) {}
}
private MultipartBody.Part getFilePart(String partName, Uri uri) {
try {
InputStream is = getContext().getContentResolver().openInputStream(uri);
String fileName = getFileName(uri);
RequestBody rb = 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(is)) { sink.writeAll(source); } }
};
return MultipartBody.Part.createFormData(partName, fileName, rb);
} catch (Exception e) { return null; }
}
private void fetchFormEntries() {
UserManager user = UserManager.getInstance();
String searchJson = "{\"field_filters\":[{\"key\":\"created_by\",\"value\":\"" + user.getUserId() + "\"}]}";
String encodedSearch = "";
try { encodedSearch = URLEncoder.encode(searchJson, "UTF-8"); } catch (Exception ignored) {}
String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch;
Request req = new Request.Builder().url(url).header("Cookie", user.getCookie()).build();
client.newCall(req).enqueue(new okhttp3.Callback() {
@Override public void onResponse(okhttp3.Call call, Response response) throws IOException {
if (response.isSuccessful()) {
try {
JSONObject json = new JSONObject(response.body().string());
if (json.has("entries") && getActivity() != null) {
JSONArray entries = json.getJSONArray("entries");
getActivity().runOnUiThread(() -> showHistory(entries));
}
} catch (Exception ignored) {}
}
}
@Override public void onFailure(okhttp3.Call call, IOException e) {}
});
}
private void showHistory(JSONArray entries) {
if (historyContainer == null) return; historyContainer.removeAllViews();
lblHistory.setVisibility(entries.length() > 0 ? View.VISIBLE : View.GONE);
try {
for (int i = 0; i < Math.min(entries.length(), 10); i++) {
JSONObject entry = entries.getJSONObject(i);
TextView item = new TextView(getContext());
item.setText("Innsendt: " + entry.optString("date_created"));
item.setPadding(10, 20, 10, 20);
item.setOnClickListener(v -> showEntryDetails(entry));
historyContainer.addView(item);
}
} catch (Exception ignored) {}
}
private void showEntryDetails(JSONObject entry) {
StringBuilder sb = new StringBuilder();
sb.append("<b>Innsendt:</b> ").append(entry.optString("date_created")).append("<br><br>");
// Enkel visning av feltverdier
JSONArray keys = entry.names();
if (keys != null) {
for (int i = 0; i < keys.length(); i++) {
String key = keys.optString(i);
if (key.matches("\\d+")) sb.append("Felt ").append(key).append(": ").append(entry.optString(key)).append("<br>");
}
}
new AlertDialog.Builder(getContext()).setTitle("Detaljer").setMessage(Html.fromHtml(sb.toString(), Html.FROM_HTML_MODE_COMPACT)).setPositiveButton("Lukk", null).show();
}
private void clearInputs() {
for (View v : inputViews.values()) {
if (v instanceof EditText) ((EditText) v).setText("");
else if (v instanceof CheckBox) ((CheckBox) v).setChecked(false);
else if (v instanceof RadioGroup) ((RadioGroup) v).clearCheck();
}
fileUploads.clear();
nestedEntries.clear();
if (nestedEntriesContainer != null) nestedEntriesContainer.removeAllViews();
}
private void updateStatus(String msg) {
if (getActivity() != null && txtStatus != null) getActivity().runOnUiThread(() -> txtStatus.setText(msg));
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (getActivity() instanceof AppCompatActivity) {
AppCompatActivity activity = (AppCompatActivity) getActivity();
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
private static class NestedEntry {
String id, description, price;
NestedEntry(String id, String d, String p) { this.id = id; this.description = d; this.price = p; }
}
}