920 lines
No EOL
43 KiB
Java
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; }
|
|
}
|
|
} |