From 71bfb191bcce08dee3c76613c2db00eb897d9f27 Mon Sep 17 00:00:00 2001 From: ErolHaagenrud Date: Wed, 17 Dec 2025 08:58:58 +0100 Subject: [PATCH] =?UTF-8?q?F=C3=B8rkalenderfargeendring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kbs/kbsintranett/AuthRepository.java | 3 + .../kbs/kbsintranett/CreateEventFragment.java | 70 +- .../com/kbs/kbsintranett/HomeFragment.java | 9 +- .../com/kbs/kbsintranett/LoginResponse.java | 12 +- .../com/kbs/kbsintranett/UserManager.java | 55 +- hele_prosjektet.txt | 1650 +++++++++++++---- 6 files changed, 1379 insertions(+), 420 deletions(-) diff --git a/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java index d9e5512..364d870 100644 --- a/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java +++ b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java @@ -54,6 +54,9 @@ public class AuthRepository { // Lagre utvidet info i UserManager UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil); + // Lagre listen over skrivbare kalendere + UserManager.getInstance().setWriteableCalendars(response.body().writeableCalendars); + callback.onSuccess(role); } else { diff --git a/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java b/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java index 5fe3036..35cb348 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java @@ -82,6 +82,12 @@ public class CreateEventFragment extends Fragment { btnSave = view.findViewById(R.id.btn_save_event); + // Initialiser tid (neste hele time) + startCal.add(Calendar.HOUR_OF_DAY, 1); + startCal.set(Calendar.MINUTE, 0); + endCal.setTime(startCal.getTime()); + endCal.add(Calendar.HOUR_OF_DAY, 1); + setupCalendarSpinner(); setupReminderChips(); @@ -90,12 +96,6 @@ public class CreateEventFragment extends Fragment { eventToEdit = (CalendarEvent) getArguments().getSerializable("edit_event"); prefillForm(eventToEdit); btnSave.setText("Oppdater Hendelse"); - } else { - // Ny event: Standard tid (neste time) - startCal.add(Calendar.HOUR_OF_DAY, 1); - startCal.set(Calendar.MINUTE, 0); - endCal.setTime(startCal.getTime()); - endCal.add(Calendar.HOUR_OF_DAY, 1); } updateRecurrenceSpinner(); @@ -115,8 +115,8 @@ public class CreateEventFragment extends Fragment { spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - // Unngå å overskrive ved innlasting - if (eventToEdit != null && position == 0) return; + // Unngå å overskrive ved innlasting hvis vi allerede har satt en verdi + if (eventToEdit != null && position == 0 && selectedRRule != null) return; String selected = parent.getItemAtPosition(position).toString(); if (selected.equals("Egendefinert...")) { @@ -152,10 +152,8 @@ public class CreateEventFragment extends Fragment { Date d = simpleFormat.parse(start); startCal.setTime(d); - // Sluttdato if (event.getRawEndDate() != null) { Date e = simpleFormat.parse(event.getRawEndDate()); - // Google sender sluttdato eksklusiv (dagen etter). Vi trekker fra 1 dag for visning. Calendar c = Calendar.getInstance(); c.setTime(e); c.add(Calendar.DAY_OF_MONTH, -1); @@ -179,23 +177,15 @@ public class CreateEventFragment extends Fragment { } } - // Varsler - Finn tag i den rå beskrivelsen - if (event.getDescription() != null) { - Pattern p = Pattern.compile("#varsel:([\\d,]+)"); - Matcher m = p.matcher(event.getDescription()); - if (m.find()) { - String[] parts = m.group(1).split(","); - // Fjern alle sjekkmerker først - for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { - ((Chip) chipGroupReminders.getChildAt(i)).setChecked(false); - } - // Sett nye - for (String part : parts) { - try { - int min = Integer.parseInt(part); - checkChipByMinutes(min); - } catch (NumberFormatException e) {} - } + // Varsler + List existingReminders = event.getReminders(); + if (!existingReminders.isEmpty()) { + // Fjern alle sjekkmerker først (15 min er default checked) + for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { + ((Chip) chipGroupReminders.getChildAt(i)).setChecked(false); + } + for (int min : existingReminders) { + checkChipByMinutes(min); } } @@ -214,13 +204,11 @@ public class CreateEventFragment extends Fragment { } private void setupCalendarSpinner() { - String[] calendars = { - "Felles", - "Administrasjonen", - "Serviceavdelingen", - "Automasjonsavdelingen", - "Prosjektavdelingen" - }; + // HENT DYNAMISK LISTE FRA USERMANAGER + List calendars = UserManager.getInstance().getWriteableCalendars(); + + if (calendars.isEmpty()) calendars.add("Felles"); + spinnerCalendar.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, calendars)); } @@ -236,7 +224,6 @@ public class CreateEventFragment extends Fragment { addChip("2 dager", 2880); addChip("1 uke", 10080); - // Marker 15 min som standard KUN for ny event if (eventToEdit == null) checkChipByMinutes(15); } @@ -474,15 +461,12 @@ public class CreateEventFragment extends Fragment { } } + // NY: Henter navnet på kalenderen direkte fra Spinneren private String getCalendarSlug() { - int pos = spinnerCalendar.getSelectedItemPosition(); - switch (pos) { - case 1: return "Administrasjonen"; - case 2: return "Serviceavdelingen"; - case 3: return "Automasjonsavdelingen"; - case 4: return "Prosjektavdelingen"; - default: return "Felles"; // case 0 + if (spinnerCalendar.getSelectedItem() != null) { + return spinnerCalendar.getSelectedItem().toString(); } + return "Felles"; } private void submitEvent() { @@ -508,14 +492,12 @@ public class CreateEventFragment extends Fragment { selectedRRule ); - // HVIS VI REDIGERER, SETT ID if (eventToEdit != null) { req.id = eventToEdit.getId(); } Toast.makeText(getContext(), eventToEdit != null ? "Oppdaterer..." : "Oppretter...", Toast.LENGTH_SHORT).show(); - // Velg riktig API-kall (Create vs Update) Call call; if (eventToEdit != null) { call = RetrofitClient.getApiService().updateCalendarEvent(req); diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index 3d9be59..9db78bf 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -3,13 +3,11 @@ package com.kbs.kbsintranett; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -65,8 +63,11 @@ public class HomeFragment extends Fragment { profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); } + // NY LOGIKK: Vis knapp hvis brukeren har tilgang til minst én kalender Button btnCreateEvent = view.findViewById(R.id.btn_create_event); - if (UserManager.getInstance().isEditorOrAbove()) { + List writeable = UserManager.getInstance().getWriteableCalendars(); + + if (writeable != null && !writeable.isEmpty()) { btnCreateEvent.setVisibility(View.VISIBLE); btnCreateEvent.setOnClickListener(v -> { Navigation.findNavController(view).navigate(R.id.action_home_to_create_event); @@ -160,7 +161,6 @@ public class HomeFragment extends Fragment { })); } else { List errorList = new ArrayList<>(); - // FIKSET HER: La til "" som 5. argument (location) errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS", "")); recyclerView.setAdapter(new CalendarAdapter(errorList, null)); } @@ -212,5 +212,4 @@ public class HomeFragment extends Fragment { .build(); WorkManager.getInstance(requireContext()).enqueue(notifRequest); } - } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java b/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java index cfbda46..f7d2ae7 100644 --- a/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java +++ b/app/src/main/java/com/kbs/kbsintranett/LoginResponse.java @@ -2,10 +2,10 @@ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; +import java.util.List; public class LoginResponse { public boolean success; - @SerializedName("full_cookie") public String fullCookie; @@ -14,19 +14,21 @@ public class LoginResponse { @SerializedName("user_id") public int userId; - // --- NYE FELTER --- @SerializedName("first_name") public String firstName; @SerializedName("last_name") public String lastName; - @SerializedName("stilling") // Sjekk at JSON-nøkkelen fra WP matcher dette + @SerializedName("stilling") public String stilling; - @SerializedName("mobiltelefon") // Sjekk at JSON-nøkkelen fra WP matcher dette + @SerializedName("mobiltelefon") public String mobiltelefon; - // ------------------ + + // NYTT FELT: Liste over kalendere brukeren kan skrive til + @SerializedName("writeable_calendars") + public List writeableCalendars; public String message; } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/UserManager.java b/app/src/main/java/com/kbs/kbsintranett/UserManager.java index 903e0d3..3b91c97 100644 --- a/app/src/main/java/com/kbs/kbsintranett/UserManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/UserManager.java @@ -2,13 +2,14 @@ package com.kbs.kbsintranett; import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; /** * UserManager fungerer som en global sesjon for appen. * Den holder på informasjon om innlogget bruker, rettigheter og autentiserings-cookie. */ public class UserManager { - private static UserManager instance; // Google Data @@ -22,15 +23,16 @@ public class UserManager { private String userRole; private String currentCookie; - // --- NYE FELTER --- + // Extended Info private String firstName; private String lastName; private String stilling; private String mobiltelefon; - private UserManager() { - // Initielt er ingen logget inn - } + // NYTT: + private List writeableCalendars = new ArrayList<>(); + + private UserManager() {} public static synchronized UserManager getInstance() { if (instance == null) { @@ -39,9 +41,6 @@ public class UserManager { return instance; } - /** - * Kalles når Google-innlogging er vellykket. - */ public void setUserData(String name, String email, String token, @Nullable String photoUrl) { this.userDisplayName = name; this.userEmail = email; @@ -49,7 +48,6 @@ public class UserManager { this.photoUrl = photoUrl; } - // --- NY METODE FOR UTVIDET INFO --- public void setExtendedUserInfo(String firstName, String lastName, String stilling, String mobiltelefon) { this.firstName = firstName; this.lastName = lastName; @@ -57,19 +55,17 @@ public class UserManager { this.mobiltelefon = mobiltelefon; } - public void setCookie(String cookie) { - this.currentCookie = cookie; + public void setWriteableCalendars(List calendars) { + this.writeableCalendars = calendars != null ? calendars : new ArrayList<>(); } - public void setUserRole(String role) { - this.userRole = role; + public List getWriteableCalendars() { + return writeableCalendars; } - public void setUserId(int id) { - this.userId = id; - } - - // ---------------- GETTERS ---------------- + public void setCookie(String cookie) { this.currentCookie = cookie; } + public void setUserRole(String role) { this.userRole = role; } + public void setUserId(int id) { this.userId = id; } public String getUserDisplayName() { return userDisplayName != null ? userDisplayName : ""; } public String getUserEmail() { return userEmail != null ? userEmail : ""; } @@ -78,31 +74,17 @@ public class UserManager { public String getCookie() { return currentCookie; } public String getUserRole() { return userRole != null ? userRole : "subscriber"; } public int getUserId() { return userId; } - - // --- NYE GETTERS --- public String getFirstName() { return firstName != null ? firstName : ""; } public String getLastName() { return lastName != null ? lastName : ""; } public String getStilling() { return stilling != null ? stilling : ""; } public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; } - // ---------------- HJELPEMETODER ---------------- - - public boolean isLoggedIn() { - return userEmail != null && !userEmail.isEmpty(); - } - - public boolean isAdmin() { - return "administrator".equalsIgnoreCase(userRole); - } - + public boolean isLoggedIn() { return userEmail != null && !userEmail.isEmpty(); } + public boolean isAdmin() { return "administrator".equalsIgnoreCase(userRole); } public boolean isEditorOrAbove() { - if (userRole == null) return false; - return userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor"); + return userRole != null && (userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor")); } - /** - * Nullstiller alt. Kalles ved utlogging. - */ public void logout() { userDisplayName = null; userEmail = null; @@ -111,11 +93,10 @@ public class UserManager { userRole = null; currentCookie = null; userId = 0; - - // Nullstill nye felter firstName = null; lastName = null; stilling = null; mobiltelefon = null; + writeableCalendars.clear(); } } \ No newline at end of file diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index b4c730f..13af76b 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -368,6 +368,9 @@ public class AuthRepository { // Lagre utvidet info i UserManager UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil); + // Lagre listen over skrivbare kalendere + UserManager.getInstance().setWriteableCalendars(response.body().writeableCalendars); + callback.onSuccess(role); } else { @@ -458,24 +461,30 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarDetailsBottomSheet.java ============================================================ package com.kbs.kbsintranett; -import android.content.Intent; +import android.app.AlertDialog; import android.os.Bundle; -import android.provider.CalendarContract; +import android.text.Html; +import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; +import com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.Collections; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { - private CalendarEvent event; public CalendarDetailsBottomSheet(CalendarEvent event) { @@ -491,20 +500,19 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { TextView time = view.findViewById(R.id.sheet_time); TextView desc = view.findViewById(R.id.sheet_desc); TextView loc = view.findViewById(R.id.sheet_location); - Button btnAdd = view.findViewById(R.id.btn_add_to_calendar); - - // Skjul knapp siden appen nå varsler automatisk (iht krav) - btnAdd.setVisibility(View.GONE); + LinearLayout adminLayout = view.findViewById(R.id.layout_admin_buttons); + Button btnDelete = view.findViewById(R.id.btn_delete); + Button btnEdit = view.findViewById(R.id.btn_edit); title.setText(event.getTitle()); time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")"); if (!event.getDescription().isEmpty()) { - // HER ER FIKSEN FOR HTML: - desc.setText(android.text.Html.fromHtml(event.getDescription(), android.text.Html.FROM_HTML_MODE_COMPACT)); + // Skjul #varsel-taggen for visning + String cleanDesc = event.getDescription().replaceAll("#varsel:[\\d,]+", "").trim(); + desc.setText(Html.fromHtml(cleanDesc, Html.FROM_HTML_MODE_COMPACT)); desc.setVisibility(View.VISIBLE); - // Gjør linker klikkbare - desc.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); + desc.setMovementMethod(LinkMovementMethod.getInstance()); } else { desc.setVisibility(View.GONE); } @@ -516,37 +524,64 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { loc.setVisibility(View.GONE); } + // Sjekk admin-rettigheter + if (UserManager.getInstance().isEditorOrAbove()) { + adminLayout.setVisibility(View.VISIBLE); + + btnDelete.setOnClickListener(v -> confirmDelete()); + + btnEdit.setOnClickListener(v -> { + // Send eventet videre til redigering + Bundle bundle = new Bundle(); + bundle.putSerializable("edit_event", event); + // Vi må navigere via parent fragmentets navController + NavHostFragment.findNavController(this).navigate(R.id.navigation_create_event, bundle); + dismiss(); + }); + } + return view; } - private void addToSystemCalendar() { - try { - SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + private void confirmDelete() { + new AlertDialog.Builder(getContext()) + .setTitle("Slett hendelse") + .setMessage("Er du sikker på at du vil slette '" + event.getTitle() + "'?") + .setPositiveButton("Slett", (dialog, which) -> deleteEvent()) + .setNegativeButton("Avbryt", null) + .show(); + } - Date startDate = apiFormat.parse(event.getRawDate()); - long startMillis = startDate.getTime(); - long endMillis = startMillis + (60 * 60 * 1000); // Default 1 time hvis slutt mangler + private void deleteEvent() { + // Vi må sende en CreateEventRequest med ID for å slette + // Vi trenger ikke fylle ut alt, bare ID og kalendertype + // Siden vi ikke vet nøyaktig hvilken kalender den kom fra (APIet gir ikke det), + // prøver vi "Felles" som default, eller prøver å slette fra ID. + // PHP-koden vi lagde støtter sletting basert på ID hvis vi sender riktig kalendertype. + // For nå antar vi "Felles" eller looper i backend. I V11.3 PHP scriptet sletter den basert på ID i valgt kalender. - if (event.getRawEndDate() != null && !event.getRawEndDate().isEmpty()) { - Date endDate = apiFormat.parse(event.getRawEndDate()); - endMillis = endDate.getTime(); + CreateEventRequest req = new CreateEventRequest( + "", "", "", "", "", "Felles", new ArrayList<>(), false, "" + ); + req.id = event.getId(); + + RetrofitClient.getApiService().deleteCalendarEvent(req).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Slettet!", Toast.LENGTH_SHORT).show(); + dismiss(); + // Her burde vi ideelt sett oppdatert listen bak, men brukeren kan dra for å oppdatere + } else { + Toast.makeText(getContext(), "Kunne ikke slette (Er det en felles-hendelse?)", Toast.LENGTH_LONG).show(); + } } - Intent intent = new Intent(Intent.ACTION_INSERT) - .setData(CalendarContract.Events.CONTENT_URI) - .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) - .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) - .putExtra(CalendarContract.Events.TITLE, event.getTitle()) - .putExtra(CalendarContract.Events.DESCRIPTION, event.getDescription()) - .putExtra(CalendarContract.Events.EVENT_LOCATION, event.getLocation()) - .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY); - - startActivity(intent); - - } catch (Exception e) { - e.printStackTrace(); - } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); + } + }); } } @@ -556,31 +591,38 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; -public class CalendarEvent { +public class CalendarEvent implements Serializable { + @SerializedName("id") + private String id; @SerializedName("title") private String title; - @SerializedName("start_date") // Juster denne nøkkelen til hva APIet faktisk returnerer (f.eks "start") + + @SerializedName("start_date") private String rawDate; - @SerializedName("end_date") // Juster nøkkel (f.eks "end") + + @SerializedName("end_date") private String rawEndDate; @SerializedName("description") private String description; + @SerializedName("location") private String location; - // --- NYTT FELT: Varsling (minutter før start) --- - // Nå henter denne verdien direkte fra "reminder_minutes" i JSON-responsen fra PHP - @SerializedName("reminder_minutes") - private int reminderMinutes = 15; // Default 15 min + // V11.0: Liste av minutter (f.eks [15, 60]) + @SerializedName("reminders") + private List reminders = new ArrayList<>(); - // --- UI-hjelpefelter (settes manuelt i appen etter parsing) --- - private String day; // F.eks "12" - private String month; // F.eks "DES" - private String time; // F.eks "10:00 - 11:30" + // UI-hjelpefelter + private String day; + private String month; + private String time; - // Konstruktør for Retrofit (Gson) + // Konstruktør public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) { this.title = title; this.rawDate = rawDate; @@ -589,33 +631,43 @@ public class CalendarEvent { this.location = location; } - // Konstruktør for manuell opprettelse (f.eks ved feil) - public CalendarEvent(String title, String time, String day, String month) { - this.title = title; - this.time = time; - this.day = day; - this.month = month; - } - + public String getId() { return id; } public String getTitle() { return title; } public String getRawDate() { return rawDate; } public String getRawEndDate() { return rawEndDate; } public String getDescription() { return description != null ? description : ""; } public String getLocation() { return location != null ? location : ""; } - // Getters og Setters for UI-felter + // Henter listen. Hvis den er null (gamle data), returner tom liste. + public List getReminders() { + return reminders != null ? reminders : new ArrayList<>(); + } + +// --- KOMPATIBILITETS-METODER (For å fikse build-feil i CalendarManager og Worker) --- + + // Brukes av CalendarManager.java for lokale events + public void setReminderMinutes(int minutes) { + this.reminders = new ArrayList<>(); + if (minutes > 0) { + this.reminders.add(minutes); + } + } + + // Brukes hvis gammel kode prøver å hente ett tall. Returnerer det første i listen. + public int getReminderMinutes() { + if (reminders != null && !reminders.isEmpty()) { + return reminders.get(0); + } + return 0; + } + + // --- UI SETTERS/GETTERS --- public String getDay() { return day; } public void setDay(String day) { this.day = day; } - public String getMonth() { return month; } public void setMonth(String month) { this.month = month; } - public String getTime() { return time; } public void setTime(String time) { this.time = time; } - - // --- NYE METODER FOR VARSLING --- - public int getReminderMinutes() { return reminderMinutes; } - public void setReminderMinutes(int minutes) { this.reminderMinutes = minutes; } } ============================================================ @@ -650,7 +702,7 @@ public class CalendarFullFragment extends Fragment { private RecyclerView recyclerView; private ProgressBar progressBar; private TextView emptyView; - private LinearLayoutManager layoutManager; // Trenger denne for å scrolle + private LinearLayoutManager layoutManager; @Nullable @Override @@ -675,24 +727,25 @@ public class CalendarFullFragment extends Fragment { private void fetchAllEvents() { progressBar.setVisibility(View.VISIBLE); - // Hent personlige hendelser (Nå med historikk) + // Hent personlige hendelser List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - // NYTT: Hent fra Google direkte - String url = CalendarManager.getGoogleCalendarUrl(); - RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { + // Hent felles hendelser fra WordPress Proxy (sikrer at vi får reminders) + RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call> call, Response> response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - // Konverter og formatér - apiEvents = CalendarManager.convertGoogleResponse(response.body()); + apiEvents = response.body(); + // Formater data for visning + for (CalendarEvent e : apiEvents) { + CalendarManager.formatEventForUI(e); + } } - // Flett og vis List allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); if (allEvents.isEmpty()) { @@ -708,13 +761,12 @@ public class CalendarFullFragment extends Fragment { }); recyclerView.setAdapter(adapter); - // --- SCROLL TIL I DAG --- scrollToToday(allEvents); } } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); @@ -737,7 +789,6 @@ public class CalendarFullFragment extends Fragment { String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); int scrollIndex = 0; - // Finn første event som er i dag eller senere for (int i = 0; i < events.size(); i++) { String raw = events.get(i).getRawDate(); if (raw != null && raw.compareTo(today) >= 0) { @@ -746,12 +797,11 @@ public class CalendarFullFragment extends Fragment { } } - // Scroll litt ned slik at "i dag" havner på toppen, men ikke helt (offset 0) - // Bruker scrollToPositionWithOffset for presisjon if (scrollIndex > 0) { layoutManager.scrollToPositionWithOffset(scrollIndex, 0); } } + } ============================================================ @@ -763,7 +813,6 @@ import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.provider.CalendarContract; -import android.util.Log; import androidx.core.content.ContextCompat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -774,96 +823,6 @@ import java.util.Locale; import java.util.TimeZone; public class CalendarManager { - - // --- KONFIGURASJON FOR GOOGLE DIREKTE-KOBLING --- - private static final String GOOGLE_CALENDAR_ID = "kbservice.no_o2bmp5f9f540vedveit51optfo@group.calendar.google.com"; - // TODO: Sett inn din API-nøkkel her! - private static final String GOOGLE_API_KEY = "AIzaSyCos8VW5mClUcuhs86gbSJo8uitY0fVPus"; - - public static String getGoogleCalendarUrl() { - long oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - String timeMin = dateFormat.format(new Date(oneYearAgo)); - - return "https://www.googleapis.com/calendar/v3/calendars/" - + GOOGLE_CALENDAR_ID - + "/events?key=" + GOOGLE_API_KEY - + "&singleEvents=true" - + "&orderBy=startTime" - + "&maxResults=250" - + "&timeMin=" + timeMin; - } - - public static List convertGoogleResponse(GoogleCalendarModels.Response response) { - List events = new ArrayList<>(); - if (response == null || response.items == null) return events; - - for (GoogleCalendarModels.Item item : response.items) { - String title = item.summary != null ? item.summary : "(Uten tittel)"; - String desc = item.description; - String loc = item.location; - - // Dato-logikk - String start = null; - String end = null; - - if (item.start != null) { - if (item.start.dateTime != null) start = item.start.dateTime; - else start = item.start.date; - } - if (item.end != null) { - if (item.end.dateTime != null) end = item.end.dateTime; - else end = item.end.date; - } - - // --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) --- - if (title.toLowerCase().contains("test")) { - Log.d("KBS_DEBUG", "--------------------------------------------------"); - Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title); - Log.d("KBS_DEBUG", " Starttid: " + start); - - if (item.reminders != null) { - Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault); - - if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { - Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:"); - for (GoogleCalendarModels.Override override : item.reminders.overrides) { - Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes); - } - } else { - Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google."); - } - } else { - Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen."); - } - Log.d("KBS_DEBUG", "--------------------------------------------------"); - } - // ------------------------------------------------- - - // Varslings-logikk - int reminderMinutes = 15; // Default fallback - - if (item.reminders != null) { - if (item.reminders.useDefault) { - reminderMinutes = 15; - } else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { - // Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter - reminderMinutes = item.reminders.overrides.get(0).minutes; - } - } - - CalendarEvent event = new CalendarEvent(title, start, end, desc, loc); - event.setReminderMinutes(reminderMinutes); - - formatEventForUI(event); - events.add(event); - } - return events; - } - - // --- RESTERENDE KODE (UENDRET) --- - private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { @@ -968,6 +927,9 @@ public class CalendarManager { } CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); + // Denne metoden eksisterer nå i CalendarEvent (se fil 1) + event.setReminderMinutes(0); + formatEventForUI(event); deviceEvents.add(event); } @@ -1055,6 +1017,7 @@ public class CalendarManager { return all; } + } ============================================================ @@ -1195,6 +1158,596 @@ public class ConditionalLogicAdapter implements JsonDeserializer updateUI()); + + btnStartDate.setOnClickListener(v -> pickDate(startCal, true)); + btnEndDate.setOnClickListener(v -> pickDate(endCal, false)); + + btnStartTime.setOnClickListener(v -> pickTime(startCal)); + btnEndTime.setOnClickListener(v -> pickTime(endCal)); + + btnSave.setOnClickListener(v -> submitEvent()); + + spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + // Unngå å overskrive ved innlasting hvis vi allerede har satt en verdi + if (eventToEdit != null && position == 0 && selectedRRule != null) return; + + String selected = parent.getItemAtPosition(position).toString(); + if (selected.equals("Egendefinert...")) { + showCustomRecurrenceDialog(); + } else if (selected.startsWith("Ikke gjenta")) { + selectedRRule = null; + } else { + generateStandardRRule(position); + } + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + private void prefillForm(CalendarEvent event) { + etTitle.setText(event.getTitle()); + + // Rens beskrivelsen for #varsel tag + String cleanDesc = event.getDescription().replaceAll("#varsel:[\\d,]+", "").trim(); + etDesc.setText(cleanDesc); + etLocation.setText(event.getLocation()); + + // Dato-parsing + try { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + + String start = event.getRawDate(); + if (start != null) { + if (start.length() == 10) { + // Heldags + switchAllDay.setChecked(true); + Date d = simpleFormat.parse(start); + startCal.setTime(d); + + if (event.getRawEndDate() != null) { + Date e = simpleFormat.parse(event.getRawEndDate()); + Calendar c = Calendar.getInstance(); + c.setTime(e); + c.add(Calendar.DAY_OF_MONTH, -1); + endCal.setTime(c.getTime()); + } else { + endCal.setTime(d); + } + } else if (start.contains("T")) { + // Vanlig tid (kutt tidssone) + if (start.length() > 19) start = start.substring(0, 19); + startCal.setTime(isoFormat.parse(start)); + + String end = event.getRawEndDate(); + if (end != null && end.contains("T")) { + if (end.length() > 19) end = end.substring(0, 19); + endCal.setTime(isoFormat.parse(end)); + } else { + endCal.setTime(startCal.getTime()); + endCal.add(Calendar.HOUR_OF_DAY, 1); + } + } + } + + // Varsler + List existingReminders = event.getReminders(); + if (!existingReminders.isEmpty()) { + // Fjern alle sjekkmerker først (15 min er default checked) + for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { + ((Chip) chipGroupReminders.getChildAt(i)).setChecked(false); + } + for (int min : existingReminders) { + checkChipByMinutes(min); + } + } + + } catch (Exception e) { + Log.e("KBS_EDIT", "Feil ved prefill", e); + } + } + + private void checkChipByMinutes(int minutes) { + for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { + Chip chip = (Chip) chipGroupReminders.getChildAt(i); + if ((int)chip.getTag() == minutes) { + chip.setChecked(true); + } + } + } + + private void setupCalendarSpinner() { + // HENT DYNAMISK LISTE FRA USERMANAGER + List calendars = UserManager.getInstance().getWriteableCalendars(); + + if (calendars.isEmpty()) calendars.add("Felles"); + + spinnerCalendar.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, calendars)); + } + + private void setupReminderChips() { + addChip("Ved start", 0); + addChip("5 min", 5); + addChip("10 min", 10); + addChip("15 min", 15); + addChip("30 min", 30); + addChip("1 time", 60); + addChip("2 timer", 120); + addChip("1 dag", 1440); + addChip("2 dager", 2880); + addChip("1 uke", 10080); + + if (eventToEdit == null) checkChipByMinutes(15); + } + + private void addChip(String text, int minutes) { + Chip chip = new Chip(getContext()); + chip.setText(text); + chip.setTag(minutes); + chip.setCheckable(true); + chip.setClickable(true); + chipGroupReminders.addView(chip); + } + + private List getSelectedReminders() { + List selected = new ArrayList<>(); + for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { + Chip chip = (Chip) chipGroupReminders.getChildAt(i); + if (chip.isChecked()) { + selected.add((Integer) chip.getTag()); + } + } + return selected; + } + + private void updateRecurrenceSpinner() { + String dayName = new SimpleDateFormat("EEEE", new Locale("no")).format(startCal.getTime()); + int dayOfMonth = startCal.get(Calendar.DAY_OF_MONTH); + String monthName = new SimpleDateFormat("MMMM", new Locale("no")).format(startCal.getTime()); + + int weekNo = (startCal.get(Calendar.DAY_OF_MONTH) - 1) / 7 + 1; + String nthDayString = "månedlig den " + weekNo + ". " + dayName + "en"; + + List options = new ArrayList<>(); + options.add("Ikke gjenta"); + options.add("Daglig"); + options.add("Ukentlig på " + dayName); + options.add("Månedlig den " + dayOfMonth + "."); + options.add(capitalize(nthDayString)); + options.add("Årlig den " + dayOfMonth + ". " + monthName); + options.add("Hver ukedag (man-fre)"); + options.add("Egendefinert..."); + + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, options); + spinnerRecurrence.setAdapter(adapter); + } + + private void generateStandardRRule(int position) { + switch (position) { + case 1: selectedRRule = "RRULE:FREQ=DAILY"; break; + case 2: selectedRRule = "RRULE:FREQ=WEEKLY"; break; + case 3: selectedRRule = "RRULE:FREQ=MONTHLY"; break; + case 4: + int weekNo = (startCal.get(Calendar.DAY_OF_MONTH) - 1) / 7 + 1; + String dayCode = getDayCode(startCal.get(Calendar.DAY_OF_WEEK)); + selectedRRule = "RRULE:FREQ=MONTHLY;BYDAY=" + weekNo + dayCode; + break; + case 5: selectedRRule = "RRULE:FREQ=YEARLY"; break; + case 6: selectedRRule = "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; break; + default: selectedRRule = null; + } + } + + private void showCustomRecurrenceDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_custom_recurrence, null); + + EditText etInterval = view.findViewById(R.id.et_interval); + Spinner spinnerFreq = view.findViewById(R.id.spinner_freq); + View layoutWeekdays = view.findViewById(R.id.layout_weekdays); + + ToggleButton[] toggles = { + view.findViewById(R.id.tg_mon), view.findViewById(R.id.tg_tue), + view.findViewById(R.id.tg_wed), view.findViewById(R.id.tg_thu), + view.findViewById(R.id.tg_fri), view.findViewById(R.id.tg_sat), + view.findViewById(R.id.tg_sun) + }; + String[] labels = {"M", "T", "O", "T", "F", "L", "S"}; + for(int i=0; i<7; i++) { toggles[i].setText(labels[i]); toggles[i].setTextOn(labels[i]); toggles[i].setTextOff(labels[i]); } + + RadioGroup rgEnd = view.findViewById(R.id.rg_end); + Button btnEndDatePicker = view.findViewById(R.id.btn_end_date_picker); + EditText etCount = view.findViewById(R.id.et_count); + Calendar customEndCal = Calendar.getInstance(); + customEndCal.add(Calendar.MONTH, 1); + + SimpleDateFormat sdf = new SimpleDateFormat("d. MMM yyyy", Locale.getDefault()); + btnEndDatePicker.setText(sdf.format(customEndCal.getTime())); + + btnEndDatePicker.setOnClickListener(v -> { + rgEnd.check(R.id.rb_date); + new DatePickerDialog(getContext(), (p, y, m, d) -> { + customEndCal.set(y, m, d); + btnEndDatePicker.setText(sdf.format(customEndCal.getTime())); + }, customEndCal.get(Calendar.YEAR), customEndCal.get(Calendar.MONTH), customEndCal.get(Calendar.DAY_OF_MONTH)).show(); + }); + + spinnerFreq.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + layoutWeekdays.setVisibility(position == 1 ? View.VISIBLE : View.GONE); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + + builder.setView(view); + AlertDialog dialog = builder.create(); + + view.findViewById(R.id.btn_cancel).setOnClickListener(v -> { + dialog.dismiss(); + spinnerRecurrence.setSelection(0); + }); + + view.findViewById(R.id.btn_done).setOnClickListener(v -> { + StringBuilder rrule = new StringBuilder("RRULE:"); + int freqPos = spinnerFreq.getSelectedItemPosition(); + String freq = ""; + switch (freqPos) { + case 0: freq = "DAILY"; break; + case 1: freq = "WEEKLY"; break; + case 2: freq = "MONTHLY"; break; + case 3: freq = "YEARLY"; break; + } + rrule.append("FREQ=").append(freq); + + String interval = etInterval.getText().toString(); + if (!interval.isEmpty() && !interval.equals("1")) { + rrule.append(";INTERVAL=").append(interval); + } + + if (freq.equals("WEEKLY")) { + List days = new ArrayList<>(); + String[] codes = {"MO", "TU", "WE", "TH", "FR", "SA", "SU"}; + for (int i=0; i<7; i++) { + if (toggles[i].isChecked()) days.add(codes[i]); + } + if (!days.isEmpty()) { + rrule.append(";BYDAY=").append(String.join(",", days)); + } + } + + int checkedId = rgEnd.getCheckedRadioButtonId(); + if (checkedId == R.id.rb_date) { + customEndCal.set(Calendar.HOUR_OF_DAY, 23); + customEndCal.set(Calendar.MINUTE, 59); + customEndCal.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + SimpleDateFormat utc = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); + utc.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + rrule.append(";UNTIL=").append(utc.format(customEndCal.getTime())); + } else if (checkedId == R.id.rb_count) { + rrule.append(";COUNT=").append(etCount.getText().toString()); + } + + selectedRRule = rrule.toString(); + isCustomRecurrence = true; + dialog.dismiss(); + }); + + dialog.show(); + } + + private void pickDate(Calendar cal, boolean isStart) { + new DatePickerDialog(getContext(), (view, y, m, d) -> { + cal.set(y, m, d); + if (isStart) { + if (endCal.before(startCal)) { + endCal.setTime(startCal.getTime()); + if (!switchAllDay.isChecked()) endCal.add(Calendar.HOUR_OF_DAY, 1); + } + updateRecurrenceSpinner(); + if (!isCustomRecurrence) spinnerRecurrence.setSelection(0); + } + updateUI(); + }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show(); + } + + private void pickTime(Calendar cal) { + new TimePickerDialog(getContext(), (view, h, m) -> { + cal.set(Calendar.HOUR_OF_DAY, h); + cal.set(Calendar.MINUTE, m); + if (cal == startCal && endCal.before(startCal)) { + endCal.setTime(startCal.getTime()); + endCal.add(Calendar.HOUR_OF_DAY, 1); + } + updateUI(); + }, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), true).show(); + } + + private void updateUI() { + boolean isAllDay = switchAllDay.isChecked(); + + btnStartTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE); + btnEndTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE); + + SimpleDateFormat dateFmt = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); + SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm", Locale.getDefault()); + + btnStartDate.setText(dateFmt.format(startCal.getTime())); + btnEndDate.setText(dateFmt.format(endCal.getTime())); + btnStartTime.setText(timeFmt.format(startCal.getTime())); + btnEndTime.setText(timeFmt.format(endCal.getTime())); + + String preview = dateFmt.format(startCal.getTime()); + if (!isAllDay) preview += " " + timeFmt.format(startCal.getTime()); + preview += " - "; + + if (isSameDay(startCal, endCal)) { + if (isAllDay) preview += " (Samme dag)"; + else preview += timeFmt.format(endCal.getTime()); + } else { + preview += dateFmt.format(endCal.getTime()); + if (!isAllDay) preview += " " + timeFmt.format(endCal.getTime()); + } + txtPreview.setText(preview); + } + + private boolean isSameDay(Calendar c1, Calendar c2) { + return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) && + c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR); + } + + private String capitalize(String s) { + if (s == null || s.isEmpty()) return s; + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + private String getDayCode(int calendarDay) { + switch (calendarDay) { + case Calendar.MONDAY: return "MO"; + case Calendar.TUESDAY: return "TU"; + case Calendar.WEDNESDAY: return "WE"; + case Calendar.THURSDAY: return "TH"; + case Calendar.FRIDAY: return "FR"; + case Calendar.SATURDAY: return "SA"; + case Calendar.SUNDAY: return "SU"; + default: return "MO"; + } + } + + // NY: Henter navnet på kalenderen direkte fra Spinneren + private String getCalendarSlug() { + if (spinnerCalendar.getSelectedItem() != null) { + return spinnerCalendar.getSelectedItem().toString(); + } + return "Felles"; + } + + private void submitEvent() { + String title = etTitle.getText().toString().trim(); + if (title.isEmpty()) { + etTitle.setError("Mangler tittel"); + return; + } + + boolean isAllDay = switchAllDay.isChecked(); + String format = isAllDay ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm"; + SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault()); + + CreateEventRequest req = new CreateEventRequest( + title, + etDesc.getText().toString(), + etLocation.getText().toString(), + sdf.format(startCal.getTime()), + sdf.format(endCal.getTime()), + getCalendarSlug(), + getSelectedReminders(), + isAllDay, + selectedRRule + ); + + if (eventToEdit != null) { + req.id = eventToEdit.getId(); + } + + Toast.makeText(getContext(), eventToEdit != null ? "Oppdaterer..." : "Oppretter...", Toast.LENGTH_SHORT).show(); + + Call call; + if (eventToEdit != null) { + call = RetrofitClient.getApiService().updateCalendarEvent(req); + } else { + call = RetrofitClient.getApiService().createCalendarEvent(req); + } + + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), eventToEdit != null ? "Hendelse oppdatert!" : "Hendelse opprettet!", Toast.LENGTH_LONG).show(); + Navigation.findNavController(getView()).navigateUp(); + } else { + String errorMsg = "Ukjent feil"; + try { + if (response.errorBody() != null) { + errorMsg = response.errorBody().string(); + } + } catch (Exception e) {} + + Log.e("KBS_ERROR", "Server svarte med feil: " + errorMsg); + Toast.makeText(getContext(), "Feil (" + response.code() + "): Sjekk Logcat", Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("KBS_ERROR", "Nettverksfeil", t); + Toast.makeText(getContext(), "Nettverksfeil: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\CreateEventRequest.java +============================================================ +package com.kbs.kbsintranett; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +public class CreateEventRequest { + @SerializedName("id") + public String id; + @SerializedName("title") + public String title; + + @SerializedName("description") + public String description; + + @SerializedName("location") + public String location; + + @SerializedName("start_time") + public String startTime; + + @SerializedName("end_time") + public String endTime; + + @SerializedName("calendar_type") + public String calendarType; + + @SerializedName("reminders") + public List reminders; // Liste, ikke int + + @SerializedName("is_all_day") + public boolean isAllDay; + + @SerializedName("recurrence") + public String recurrence; + + // Oppdatert konstruktør som tar imot List + public CreateEventRequest(String title, String description, String location, String startTime, String endTime, String calendarType, List reminders, boolean isAllDay, String recurrence) { + this.title = title; + this.description = description; + this.location = location; + this.startTime = startTime; + this.endTime = endTime; + this.calendarType = calendarType; + this.reminders = reminders; + this.isAllDay = isAllDay; + this.recurrence = recurrence; + } +} + + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsFragment.java ============================================================ @@ -3967,6 +4520,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -3991,24 +4545,20 @@ import retrofit2.Callback; import retrofit2.Response; public class HomeFragment extends Fragment { - private ActivityResultLauncher requestPermissionLauncher; private RecyclerView calendarRecycler; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Håndter svar på kalendertillatelse requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { - // Prøv å laste kalender på nytt (nå potensielt med personlig kalender) if (calendarRecycler != null) { fetchCalendarEvents(calendarRecycler); } } ); - // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) startNotificationWorker(); } @@ -4021,45 +4571,51 @@ public class HomeFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // 0. Profil-knapp + View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); } - // 1. Kalender oppsett + // NY LOGIKK: Vis knapp hvis brukeren har tilgang til minst én kalender + Button btnCreateEvent = view.findViewById(R.id.btn_create_event); + List writeable = UserManager.getInstance().getWriteableCalendars(); + + if (writeable != null && !writeable.isEmpty()) { + btnCreateEvent.setVisibility(View.VISIBLE); + btnCreateEvent.setOnClickListener(v -> { + Navigation.findNavController(view).navigate(R.id.action_home_to_create_event); + }); + } else { + btnCreateEvent.setVisibility(View.GONE); + } + calendarRecycler = view.findViewById(R.id.recycler_calendar); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); - // Sett tom adapter midlertidig calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); - // "Se alle" knapp for kalender + TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); if (viewAllCalendar != null) { viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull)); } - // Sjekk tillatelse for personlig kalender if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { fetchCalendarEvents(calendarRecycler); } else { - // Spør om lov til å lese kalender requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); } - // Spør også om lov til å sende varsler (Android 13+) if (android.os.Build.VERSION.SDK_INT >= 33) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS); } } - // 2. Nyheter oppsett RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); newsRecycler.setNestedScrollingEnabled(false); - // Sett tom adapter midlertidig newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); - // "Se alle" knapp for nyheter + TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); if (viewAllNews != null) { viewAllNews.setOnClickListener(v -> { @@ -4071,26 +4627,22 @@ public class HomeFragment extends Fragment { } private void fetchCalendarEvents(RecyclerView recyclerView) { - // 1. Hent personlige hendelser først (fra CalendarManager) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - // 2. Hent API-hendelser DIREKTE fra Google - String url = CalendarManager.getGoogleCalendarUrl(); - RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { + RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call> call, Response> response) { if (!isAdded()) return; List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - // Konverter fra Google-modell til KBS-modell - apiEvents = CalendarManager.convertGoogleResponse(response.body()); + apiEvents = response.body(); + for (CalendarEvent e : apiEvents) { + CalendarManager.formatEventForUI(e); + } } - // 3. Flett listene (API + Personlig) og sorter List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - - // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) List upcomingEvents = new ArrayList<>(); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); @@ -4100,7 +4652,6 @@ public class HomeFragment extends Fragment { } } - // 5. Vis kun de 5 første av de kommende List top5 = new ArrayList<>(); for(int i=0; i call, Throwable t) { + public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; - // Hvis API feiler, vis bare personlige events hvis vi har noen if (!deviceEvents.isEmpty()) { List top5 = new ArrayList<>(); for(int i=0; i errorList = new ArrayList<>(); - errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS")); + errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS", "")); recyclerView.setAdapter(new CalendarAdapter(errorList, null)); } } @@ -4134,7 +4684,6 @@ public class HomeFragment extends Fragment { private void fetchNewsFromWordpress(RecyclerView recyclerView) { WordPressApiService apiService = RetrofitClient.getApiService(); - // Bruker getPosts som henter 5-10 innlegg med ?_embed apiService.getPosts().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -4142,8 +4691,6 @@ public class HomeFragment extends Fragment { if (response.isSuccessful() && response.body() != null) { List wpPosts = response.body(); - - // Datoformatering SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); @@ -4153,15 +4700,12 @@ public class HomeFragment extends Fragment { try { Date date = rawFormat.parse(post.date); post.date = targetFormat.format(date); - // Setter pen dato } catch (Exception e) {} } - // Sett adapter med Click Listener NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { - // Naviger til detaljvisning og send med post-objektet Bundle bundle = new Bundle(); - bundle.putSerializable("post_data", post); // WpPost er nå Serializable + bundle.putSerializable("post_data", post); Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); }); recyclerView.setAdapter(adapter); @@ -4171,14 +4715,12 @@ public class HomeFragment extends Fragment { @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; - // Ved feil, sett tom liste (eller vis feilmelding) recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); } }); } private void startNotificationWorker() { - // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter i HMS-kalenderen PeriodicWorkRequest notifRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) .build(); @@ -4541,10 +5083,10 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; +import java.util.List; public class LoginResponse { public boolean success; - @SerializedName("full_cookie") public String fullCookie; @@ -4553,19 +5095,21 @@ public class LoginResponse { @SerializedName("user_id") public int userId; - // --- NYE FELTER --- @SerializedName("first_name") public String firstName; @SerializedName("last_name") public String lastName; - @SerializedName("stilling") // Sjekk at JSON-nøkkelen fra WP matcher dette + @SerializedName("stilling") public String stilling; - @SerializedName("mobiltelefon") // Sjekk at JSON-nøkkelen fra WP matcher dette + @SerializedName("mobiltelefon") public String mobiltelefon; - // ------------------ + + // NYTT FELT: Liste over kalendere brukeren kan skrive til + @SerializedName("writeable_calendars") + public List writeableCalendars; public String message; } @@ -5152,6 +5696,7 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -5165,8 +5710,8 @@ import java.util.Locale; import retrofit2.Response; public class NotificationWorker extends Worker { - - private static final String TAG = "KBS_DEBUG"; + private static final String TAG = "NotificationWorker"; + private static final String PREFS_NAME = "kbs_alarm_history"; public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); @@ -5175,22 +5720,16 @@ public class NotificationWorker extends Worker { @NonNull @Override public Result doWork() { - Log.d(TAG, "NotificationWorker: Starter sjekk av kalender..."); - try { - String url = CalendarManager.getGoogleCalendarUrl(); - Response response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute(); - + Response> response = RetrofitClient.getApiService().getCalendarEvents().execute(); if (response.isSuccessful() && response.body() != null) { - List events = CalendarManager.convertGoogleResponse(response.body()); - scheduleAlarms(events); + scheduleAlarms(response.body()); return Result.success(); } else { - Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code()); + if (response.code() >= 400 && response.code() < 500) return Result.failure(); return Result.retry(); } } catch (IOException e) { - Log.e(TAG, "NotificationWorker: Nettverksfeil", e); return Result.retry(); } } @@ -5198,21 +5737,19 @@ public class NotificationWorker extends Worker { private void scheduleAlarms(List events) { Context context = getApplicationContext(); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (!alarmManager.canScheduleExactAlarms()) { - Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!"); - return; - } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) return; long now = System.currentTimeMillis(); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - int countSet = 0; + + long catchUpWindow = now - (30 * 60 * 1000L); + long futureWindow = now + (30 * 24 * 60 * 60 * 1000L); // 30 dager frem for (CalendarEvent event : events) { try { - if (event.getRawDate().length() == 10) continue; + if (event.getRawDate() == null || event.getRawDate().length() == 10) continue; Date eventDate = null; if (event.getRawDate().contains("T")) { @@ -5220,62 +5757,55 @@ public class NotificationWorker extends Worker { if (raw.length() > 19) raw = raw.substring(0, 19); eventDate = isoFormat.parse(raw); } - if (eventDate == null) continue; - long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L); + // Loop gjennom alle varsler for denne hendelsen + for (int minutesBefore : event.getReminders()) { + if (minutesBefore < 0) continue; // 0 betyr nå "ved start", negative ignoreres - // --- DETALJERT LOGGING FOR Å FEILSØKE --- - if (event.getTitle().toLowerCase().contains("test")) { - Log.d(TAG, "SJEKKER TEST-EVENT:"); - Log.d(TAG, " Start: " + eventDate); - Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før"); - Log.d(TAG, " Alarmtid: " + new Date(triggerTime)); - Log.d(TAG, " Nå: " + new Date(now)); + long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L); - if (triggerTime > now) { - Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)"); - } else { - Log.d(TAG, " Status: I FORTIDEN (Hoppes over)"); - } - } - // ---------------------------------------- + // Unik nøkkel for denne alarmen: EventID + Tidspunkt + String alarmKey = "alarm_" + event.getId() + "_" + triggerTime; - if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) { - - String uniqueIdString = event.getTitle() + event.getRawDate(); - int alarmId = uniqueIdString.hashCode(); - - Intent intent = new Intent(context, AlarmReceiver.class); - intent.putExtra("TITLE", event.getTitle()); - intent.putExtra("MESSAGE", "Starter kl " + event.getTime()); - intent.putExtra("ID", alarmId); - - PendingIntent pendingIntent = PendingIntent.getBroadcast( - context, - alarmId, - intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); - } else { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); + // Sjekk om vi allerede har fyrt denne alarmen + if (prefs.getBoolean(alarmKey, false)) { + continue; // Allerede håndtert } - Log.i(TAG, ">>> ALARM SATT: " + event.getTitle()); - countSet++; - } + if (triggerTime > catchUpWindow && triggerTime < futureWindow) { + if (triggerTime < now) { + triggerTime = now + 1000; // Catch-up + } + int alarmId = alarmKey.hashCode(); + Intent intent = new Intent(context, AlarmReceiver.class); + intent.putExtra("TITLE", event.getTitle()); + String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate); + intent.putExtra("MESSAGE", "Starter kl " + timeStr); + intent.putExtra("ID", alarmId); + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); + } else { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); + } + + // Marker som satt + prefs.edit().putBoolean(alarmKey, true).apply(); + Log.i(TAG, "Satt alarm: " + event.getTitle() + " (" + minutesBefore + "m før)"); + } + } } catch (Exception e) { - Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e); + Log.e(TAG, "Feil", e); } } - if (countSet == 0) { - Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t)."); - } + // Rensk opp gamle nøkler (valgfritt, for å spare plass over tid) } } @@ -5400,15 +5930,15 @@ import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitClient { private static final String BASE_URL = "https://intranet.kbs.no/"; private static Retrofit retrofit = null; - public static WordPressApiService getApiService() { if (retrofit == null) { - // ENDRET: Redusert loggnivå fra BODY til BASIC for å unngå spam i Logcat HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); if (BuildConfig.DEBUG) { + // I debug-modus logger vi det mest nødvendige logging.setLevel(HttpLoggingInterceptor.Level.BASIC); } else { + // I release er vi stille for ytelse og sikkerhet logging.setLevel(HttpLoggingInterceptor.Level.NONE); } @@ -5456,13 +5986,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java package com.kbs.kbsintranett; import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; /** * UserManager fungerer som en global sesjon for appen. * Den holder på informasjon om innlogget bruker, rettigheter og autentiserings-cookie. */ public class UserManager { - private static UserManager instance; // Google Data @@ -5476,15 +6007,16 @@ public class UserManager { private String userRole; private String currentCookie; - // --- NYE FELTER --- + // Extended Info private String firstName; private String lastName; private String stilling; private String mobiltelefon; - private UserManager() { - // Initielt er ingen logget inn - } + // NYTT: + private List writeableCalendars = new ArrayList<>(); + + private UserManager() {} public static synchronized UserManager getInstance() { if (instance == null) { @@ -5493,9 +6025,6 @@ public class UserManager { return instance; } - /** - * Kalles når Google-innlogging er vellykket. - */ public void setUserData(String name, String email, String token, @Nullable String photoUrl) { this.userDisplayName = name; this.userEmail = email; @@ -5503,7 +6032,6 @@ public class UserManager { this.photoUrl = photoUrl; } - // --- NY METODE FOR UTVIDET INFO --- public void setExtendedUserInfo(String firstName, String lastName, String stilling, String mobiltelefon) { this.firstName = firstName; this.lastName = lastName; @@ -5511,19 +6039,17 @@ public class UserManager { this.mobiltelefon = mobiltelefon; } - public void setCookie(String cookie) { - this.currentCookie = cookie; + public void setWriteableCalendars(List calendars) { + this.writeableCalendars = calendars != null ? calendars : new ArrayList<>(); } - public void setUserRole(String role) { - this.userRole = role; + public List getWriteableCalendars() { + return writeableCalendars; } - public void setUserId(int id) { - this.userId = id; - } - - // ---------------- GETTERS ---------------- + public void setCookie(String cookie) { this.currentCookie = cookie; } + public void setUserRole(String role) { this.userRole = role; } + public void setUserId(int id) { this.userId = id; } public String getUserDisplayName() { return userDisplayName != null ? userDisplayName : ""; } public String getUserEmail() { return userEmail != null ? userEmail : ""; } @@ -5532,31 +6058,17 @@ public class UserManager { public String getCookie() { return currentCookie; } public String getUserRole() { return userRole != null ? userRole : "subscriber"; } public int getUserId() { return userId; } - - // --- NYE GETTERS --- public String getFirstName() { return firstName != null ? firstName : ""; } public String getLastName() { return lastName != null ? lastName : ""; } public String getStilling() { return stilling != null ? stilling : ""; } public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; } - // ---------------- HJELPEMETODER ---------------- - - public boolean isLoggedIn() { - return userEmail != null && !userEmail.isEmpty(); - } - - public boolean isAdmin() { - return "administrator".equalsIgnoreCase(userRole); - } - + public boolean isLoggedIn() { return userEmail != null && !userEmail.isEmpty(); } + public boolean isAdmin() { return "administrator".equalsIgnoreCase(userRole); } public boolean isEditorOrAbove() { - if (userRole == null) return false; - return userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor"); + return userRole != null && (userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor")); } - /** - * Nullstiller alt. Kalles ved utlogging. - */ public void logout() { userDisplayName = null; userEmail = null; @@ -5565,12 +6077,11 @@ public class UserManager { userRole = null; currentCookie = null; userId = 0; - - // Nullstill nye felter firstName = null; lastName = null; stilling = null; mobiltelefon = null; + writeableCalendars.clear(); } } @@ -5638,7 +6149,7 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\WordPressApiService.java package com.kbs.kbsintranett; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; // NY IMPORT +import com.google.gson.JsonObject; import java.util.List; import java.util.Map; import okhttp3.MultipartBody; @@ -5652,12 +6163,10 @@ import retrofit2.http.Multipart; import retrofit2.http.Part; import retrofit2.http.PartMap; import retrofit2.http.Query; -import retrofit2.http.Url; // NY IMPORT public interface WordPressApiService { @GET("wp-json/wp/v2/posts?per_page=10&_embed") Call> getPosts(); - @GET("wp-json/kbs/v1/forms/{id}") Call getForm(@Path("id") int formId); @@ -5678,13 +6187,12 @@ public interface WordPressApiService { @Part List files ); - // ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); - // NY: Direkte kall mot Google (bruker @Url for å override base URL) - @GET - Call getDirectGoogleEvents(@Url String fullUrl); + // DETTE ER METODEN SOM MANGLER: + @POST("wp-json/kbs/v1/calendar/create") + Call createCalendarEvent(@Body CreateEventRequest request); @GET("wp-json/gf/v2/entries") Call getEntries( @@ -5705,9 +6213,16 @@ public interface WordPressApiService { @GET("wp-json/kbs/v1/handbook/{id}") Call getHandbookPage(@Path("id") int id); - // NY: Slå opp ID fra URL @GET("wp-json/kbs/v1/lookup-id") Call lookupPageId(@Query("url") String url); + + @POST("wp-json/kbs/v1/calendar/update") + Call updateCalendarEvent(@Body CreateEventRequest request); + + @POST("wp-json/kbs/v1/calendar/delete") + Call deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type + + } ============================================================ @@ -5820,6 +6335,15 @@ public class WpPost implements Serializable { } } +============================================================ +FILSTI: app\src\main\res\color\selector_day_text.xml +============================================================ + + + + + + ============================================================ FILSTI: app\src\main\res\drawable\bg_category_selected.xml ============================================================ @@ -6054,6 +6578,24 @@ FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml android:strokeColor="#00000000" /> +============================================================ +FILSTI: app\src\main\res\drawable\selector_day_toggle.xml +============================================================ + + + + + + + + + + + + + + + ============================================================ FILSTI: app\src\main\res\layout\activity_main.xml ============================================================ @@ -6125,7 +6667,6 @@ FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml android:orientation="vertical" android:padding="24dp" android:background="@android:color/white"> - + android:layout_marginBottom="24dp"/> + + + +