Før tilgangskontroll til kalendre

This commit is contained in:
ErolHaagenrud 2025-12-17 08:04:08 +01:00
parent fb9749ba3f
commit 3b2d19bd0b
10 changed files with 419 additions and 187 deletions

View file

@ -1,23 +1,29 @@
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import android.content.Intent; import android.app.AlertDialog;
import android.os.Bundle; import android.os.Bundle;
import android.provider.CalendarContract; import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.text.SimpleDateFormat; import com.google.gson.JsonElement;
import java.util.Date; import java.util.ArrayList;
import java.util.Locale; import java.util.Collections;
import java.util.TimeZone; import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment {
private CalendarEvent event; private CalendarEvent event;
public CalendarDetailsBottomSheet(CalendarEvent event) { public CalendarDetailsBottomSheet(CalendarEvent event) {
@ -33,20 +39,19 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment {
TextView time = view.findViewById(R.id.sheet_time); TextView time = view.findViewById(R.id.sheet_time);
TextView desc = view.findViewById(R.id.sheet_desc); TextView desc = view.findViewById(R.id.sheet_desc);
TextView loc = view.findViewById(R.id.sheet_location); TextView loc = view.findViewById(R.id.sheet_location);
Button btnAdd = view.findViewById(R.id.btn_add_to_calendar); LinearLayout adminLayout = view.findViewById(R.id.layout_admin_buttons);
Button btnDelete = view.findViewById(R.id.btn_delete);
// Skjul knapp siden appen varsler automatisk (iht krav) Button btnEdit = view.findViewById(R.id.btn_edit);
btnAdd.setVisibility(View.GONE);
title.setText(event.getTitle()); title.setText(event.getTitle());
time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")"); time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")");
if (!event.getDescription().isEmpty()) { if (!event.getDescription().isEmpty()) {
// HER ER FIKSEN FOR HTML: // Skjul #varsel-taggen for visning
desc.setText(android.text.Html.fromHtml(event.getDescription(), android.text.Html.FROM_HTML_MODE_COMPACT)); String cleanDesc = event.getDescription().replaceAll("#varsel:[\\d,]+", "").trim();
desc.setText(Html.fromHtml(cleanDesc, Html.FROM_HTML_MODE_COMPACT));
desc.setVisibility(View.VISIBLE); desc.setVisibility(View.VISIBLE);
// Gjør linker klikkbare desc.setMovementMethod(LinkMovementMethod.getInstance());
desc.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
} else { } else {
desc.setVisibility(View.GONE); desc.setVisibility(View.GONE);
} }
@ -58,36 +63,63 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment {
loc.setVisibility(View.GONE); 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 navigere via parent fragmentets navController
NavHostFragment.findNavController(this).navigate(R.id.navigation_create_event, bundle);
dismiss();
});
}
return view; return view;
} }
private void addToSystemCalendar() { private void confirmDelete() {
try { new AlertDialog.Builder(getContext())
SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); .setTitle("Slett hendelse")
apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); .setMessage("Er du sikker på at du vil slette '" + event.getTitle() + "'?")
.setPositiveButton("Slett", (dialog, which) -> deleteEvent())
Date startDate = apiFormat.parse(event.getRawDate()); .setNegativeButton("Avbryt", null)
long startMillis = startDate.getTime(); .show();
long endMillis = startMillis + (60 * 60 * 1000); // Default 1 time hvis slutt mangler
if (event.getRawEndDate() != null && !event.getRawEndDate().isEmpty()) {
Date endDate = apiFormat.parse(event.getRawEndDate());
endMillis = endDate.getTime();
} }
Intent intent = new Intent(Intent.ACTION_INSERT) private void deleteEvent() {
.setData(CalendarContract.Events.CONTENT_URI) // Vi sende en CreateEventRequest med ID for å slette
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) // Vi trenger ikke fylle ut alt, bare ID og kalendertype
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) // Siden vi ikke vet nøyaktig hvilken kalender den kom fra (APIet gir ikke det),
.putExtra(CalendarContract.Events.TITLE, event.getTitle()) // prøver vi "Felles" som default, eller prøver å slette fra ID.
.putExtra(CalendarContract.Events.DESCRIPTION, event.getDescription()) // PHP-koden vi lagde støtter sletting basert ID hvis vi sender riktig kalendertype.
.putExtra(CalendarContract.Events.EVENT_LOCATION, event.getLocation()) // For antar vi "Felles" eller looper i backend. I V11.3 PHP scriptet sletter den basert ID i valgt kalender.
.putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY);
startActivity(intent); CreateEventRequest req = new CreateEventRequest(
"", "", "", "", "", "Felles", new ArrayList<>(), false, ""
);
req.id = event.getId();
} catch (Exception e) { RetrofitClient.getApiService().deleteCalendarEvent(req).enqueue(new Callback<JsonElement>() {
e.printStackTrace(); @Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> 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();
} }
} }
@Override
public void onFailure(Call<JsonElement> call, Throwable t) {
Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show();
}
});
}
} }

View file

@ -1,31 +1,38 @@
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName; 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") @SerializedName("title")
private String 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; private String rawDate;
@SerializedName("end_date") // Juster nøkkel (f.eks "end")
@SerializedName("end_date")
private String rawEndDate; private String rawEndDate;
@SerializedName("description") @SerializedName("description")
private String description; private String description;
@SerializedName("location") @SerializedName("location")
private String location; private String location;
// --- NYTT FELT: Varsling (minutter før start) --- // V11.0: Liste av minutter (f.eks [15, 60])
// henter denne verdien direkte fra "reminder_minutes" i JSON-responsen fra PHP @SerializedName("reminders")
@SerializedName("reminder_minutes") private List<Integer> reminders = new ArrayList<>();
private int reminderMinutes = 15; // Default 15 min
// --- UI-hjelpefelter (settes manuelt i appen etter parsing) --- // UI-hjelpefelter
private String day; // F.eks "12" private String day;
private String month; // F.eks "DES" private String month;
private String time; // F.eks "10:00 - 11:30" private String time;
// Konstruktør for Retrofit (Gson) // Konstruktør
public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) { public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) {
this.title = title; this.title = title;
this.rawDate = rawDate; this.rawDate = rawDate;
@ -34,31 +41,41 @@ public class CalendarEvent {
this.location = location; this.location = location;
} }
// Konstruktør for manuell opprettelse (f.eks ved feil) public String getId() { return id; }
public CalendarEvent(String title, String time, String day, String month) {
this.title = title;
this.time = time;
this.day = day;
this.month = month;
}
public String getTitle() { return title; } public String getTitle() { return title; }
public String getRawDate() { return rawDate; } public String getRawDate() { return rawDate; }
public String getRawEndDate() { return rawEndDate; } public String getRawEndDate() { return rawEndDate; }
public String getDescription() { return description != null ? description : ""; } public String getDescription() { return description != null ? description : ""; }
public String getLocation() { return location != null ? location : ""; } 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<Integer> 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 String getDay() { return day; }
public void setDay(String day) { this.day = day; } public void setDay(String day) { this.day = day; }
public String getMonth() { return month; } public String getMonth() { return month; }
public void setMonth(String month) { this.month = month; } public void setMonth(String month) { this.month = month; }
public String getTime() { return time; } public String getTime() { return time; }
public void setTime(String time) { this.time = 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; }
} }

View file

@ -118,7 +118,8 @@ public class CalendarManager {
} }
CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc);
event.setReminderMinutes(0); // Systemet håndterer lokale varsler // Denne metoden eksisterer i CalendarEvent (se fil 1)
event.setReminderMinutes(0);
formatEventForUI(event); formatEventForUI(event);
deviceEvents.add(event); deviceEvents.add(event);

View file

@ -22,19 +22,25 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class CreateEventFragment extends Fragment { public class CreateEventFragment extends Fragment {
private EditText etTitle, etDesc, etLocation; private EditText etTitle, etDesc, etLocation;
private Spinner spinnerCalendar, spinnerReminder, spinnerRecurrence; private Spinner spinnerCalendar, spinnerRecurrence;
private ChipGroup chipGroupReminders;
private Switch switchAllDay; private Switch switchAllDay;
private TextView txtPreview; private TextView txtPreview;
private Button btnStartDate, btnStartTime, btnEndDate, btnEndTime, btnSave; private Button btnStartDate, btnStartTime, btnEndDate, btnEndTime, btnSave;
@ -45,6 +51,9 @@ public class CreateEventFragment extends Fragment {
private String selectedRRule = null; private String selectedRRule = null;
private boolean isCustomRecurrence = false; private boolean isCustomRecurrence = false;
// EDIT MODE
private CalendarEvent eventToEdit = null;
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -61,8 +70,8 @@ public class CreateEventFragment extends Fragment {
switchAllDay = view.findViewById(R.id.switch_all_day); switchAllDay = view.findViewById(R.id.switch_all_day);
spinnerCalendar = view.findViewById(R.id.spinner_calendar); spinnerCalendar = view.findViewById(R.id.spinner_calendar);
spinnerReminder = view.findViewById(R.id.spinner_reminder);
spinnerRecurrence = view.findViewById(R.id.spinner_recurrence); spinnerRecurrence = view.findViewById(R.id.spinner_recurrence);
chipGroupReminders = view.findViewById(R.id.chip_group_reminders);
txtPreview = view.findViewById(R.id.txt_time_preview); txtPreview = view.findViewById(R.id.txt_time_preview);
@ -73,13 +82,22 @@ public class CreateEventFragment extends Fragment {
btnSave = view.findViewById(R.id.btn_save_event); btnSave = view.findViewById(R.id.btn_save_event);
// Initialiser tid (neste hele time) setupCalendarSpinner();
setupReminderChips();
// SJEKK OM VI ER I REDIGERINGS-MODUS
if (getArguments() != null && getArguments().containsKey("edit_event")) {
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.add(Calendar.HOUR_OF_DAY, 1);
startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.MINUTE, 0);
endCal.setTime(startCal.getTime()); endCal.setTime(startCal.getTime());
endCal.add(Calendar.HOUR_OF_DAY, 1); endCal.add(Calendar.HOUR_OF_DAY, 1);
}
setupStandardSpinners();
updateRecurrenceSpinner(); updateRecurrenceSpinner();
updateUI(); updateUI();
@ -97,8 +115,10 @@ public class CreateEventFragment extends Fragment {
spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String selected = parent.getItemAtPosition(position).toString(); // Unngå å overskrive ved innlasting
if (eventToEdit != null && position == 0) return;
String selected = parent.getItemAtPosition(position).toString();
if (selected.equals("Egendefinert...")) { if (selected.equals("Egendefinert...")) {
showCustomRecurrenceDialog(); showCustomRecurrenceDialog();
} else if (selected.startsWith("Ikke gjenta")) { } else if (selected.startsWith("Ikke gjenta")) {
@ -111,8 +131,89 @@ public class CreateEventFragment extends Fragment {
}); });
} }
private void setupStandardSpinners() { private void prefillForm(CalendarEvent event) {
// matche PHP switch-case nøyaktig 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);
// 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);
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 - Finn tag i den 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) {}
}
}
}
} 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() {
String[] calendars = { String[] calendars = {
"Felles", "Felles",
"Administrasjonen", "Administrasjonen",
@ -121,11 +222,42 @@ public class CreateEventFragment extends Fragment {
"Prosjektavdelingen" "Prosjektavdelingen"
}; };
spinnerCalendar.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, calendars)); spinnerCalendar.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, calendars));
}
String[] reminders = {"Ingen varsling", "10 min før", "15 min før", "30 min før", "1 time før", "2 timer før", "24 timer før"}; private void setupReminderChips() {
ArrayAdapter<String> remAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, reminders); addChip("Ved start", 0);
spinnerReminder.setAdapter(remAdapter); addChip("5 min", 5);
spinnerReminder.setSelection(2); // 15 min default 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);
// Marker 15 min som standard KUN for ny event
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<Integer> getSelectedReminders() {
List<Integer> 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() { private void updateRecurrenceSpinner() {
@ -353,19 +485,6 @@ public class CreateEventFragment extends Fragment {
} }
} }
private int getReminderMinutes() {
int pos = spinnerReminder.getSelectedItemPosition();
switch (pos) {
case 1: return 10;
case 2: return 15;
case 3: return 30;
case 4: return 60;
case 5: return 120;
case 6: return 1440;
default: return 0;
}
}
private void submitEvent() { private void submitEvent() {
String title = etTitle.getText().toString().trim(); String title = etTitle.getText().toString().trim();
if (title.isEmpty()) { if (title.isEmpty()) {
@ -384,33 +503,41 @@ public class CreateEventFragment extends Fragment {
sdf.format(startCal.getTime()), sdf.format(startCal.getTime()),
sdf.format(endCal.getTime()), sdf.format(endCal.getTime()),
getCalendarSlug(), getCalendarSlug(),
getReminderMinutes(), getSelectedReminders(),
isAllDay, isAllDay,
selectedRRule selectedRRule
); );
Toast.makeText(getContext(), "Oppretter...", Toast.LENGTH_SHORT).show(); // HVIS VI REDIGERER, SETT ID
if (eventToEdit != null) {
req.id = eventToEdit.getId();
}
// HER ER DEN NYE LOGIKKEN FOR Å LESE FEILMELDING FRA PHP Toast.makeText(getContext(), eventToEdit != null ? "Oppdaterer..." : "Oppretter...", Toast.LENGTH_SHORT).show();
RetrofitClient.getApiService().createCalendarEvent(req).enqueue(new Callback<JsonElement>() {
// Velg riktig API-kall (Create vs Update)
Call<JsonElement> call;
if (eventToEdit != null) {
call = RetrofitClient.getApiService().updateCalendarEvent(req);
} else {
call = RetrofitClient.getApiService().createCalendarEvent(req);
}
call.enqueue(new Callback<JsonElement>() {
@Override @Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> response) { public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
Toast.makeText(getContext(), "Hendelse opprettet!", Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), eventToEdit != null ? "Hendelse oppdatert!" : "Hendelse opprettet!", Toast.LENGTH_LONG).show();
Navigation.findNavController(getView()).navigateUp(); Navigation.findNavController(getView()).navigateUp();
} else { } else {
// Les feilmelding fra serveren (body)
String errorMsg = "Ukjent feil"; String errorMsg = "Ukjent feil";
try { try {
if (response.errorBody() != null) { if (response.errorBody() != null) {
errorMsg = response.errorBody().string(); errorMsg = response.errorBody().string();
} }
} catch (Exception e) { } catch (Exception e) {}
errorMsg = "Kunne ikke lese feil: " + e.getMessage();
}
Log.e("KBS_ERROR", "Server svarte med feil: " + errorMsg); Log.e("KBS_ERROR", "Server svarte med feil: " + errorMsg);
// Vis en kortversjon til brukeren, eller hele hvis du debugger
Toast.makeText(getContext(), "Feil (" + response.code() + "): Sjekk Logcat", Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), "Feil (" + response.code() + "): Sjekk Logcat", Toast.LENGTH_LONG).show();
} }
} }
@ -422,5 +549,4 @@ public class CreateEventFragment extends Fragment {
} }
}); });
} }
} }

View file

@ -1,7 +1,10 @@
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import java.util.List;
public class CreateEventRequest { public class CreateEventRequest {
@SerializedName("id")
public String id;
@SerializedName("title") @SerializedName("title")
public String title; public String title;
@ -9,7 +12,7 @@ public class CreateEventRequest {
public String description; public String description;
@SerializedName("location") @SerializedName("location")
public String location; // NY public String location;
@SerializedName("start_time") @SerializedName("start_time")
public String startTime; public String startTime;
@ -20,23 +23,24 @@ public class CreateEventRequest {
@SerializedName("calendar_type") @SerializedName("calendar_type")
public String calendarType; public String calendarType;
@SerializedName("reminder_minutes") @SerializedName("reminders")
public int reminderMinutes; public List<Integer> reminders; // Liste, ikke int
@SerializedName("is_all_day") @SerializedName("is_all_day")
public boolean isAllDay; // NY public boolean isAllDay;
@SerializedName("recurrence") @SerializedName("recurrence")
public String recurrence; // NY (RRULE) public String recurrence;
public CreateEventRequest(String title, String description, String location, String startTime, String endTime, String calendarType, int reminderMinutes, boolean isAllDay, String recurrence) { // Oppdatert konstruktør som tar imot List<Integer>
public CreateEventRequest(String title, String description, String location, String startTime, String endTime, String calendarType, List<Integer> reminders, boolean isAllDay, String recurrence) {
this.title = title; this.title = title;
this.description = description; this.description = description;
this.location = location; this.location = location;
this.startTime = startTime; this.startTime = startTime;
this.endTime = endTime; this.endTime = endTime;
this.calendarType = calendarType; this.calendarType = calendarType;
this.reminderMinutes = reminderMinutes; this.reminders = reminders;
this.isAllDay = isAllDay; this.isAllDay = isAllDay;
this.recurrence = recurrence; this.recurrence = recurrence;
} }

View file

@ -160,7 +160,8 @@ public class HomeFragment extends Fragment {
})); }));
} else { } else {
List<CalendarEvent> errorList = new ArrayList<>(); List<CalendarEvent> errorList = new ArrayList<>();
errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS")); // 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)); recyclerView.setAdapter(new CalendarAdapter(errorList, null));
} }
} }

View file

@ -4,6 +4,7 @@ import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -18,6 +19,7 @@ import retrofit2.Response;
public class NotificationWorker extends Worker { public class NotificationWorker extends Worker {
private static final String TAG = "NotificationWorker"; private static final String TAG = "NotificationWorker";
private static final String PREFS_NAME = "kbs_alarm_history";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams); super(context, workerParams);
@ -28,20 +30,14 @@ public class NotificationWorker extends Worker {
public Result doWork() { public Result doWork() {
try { try {
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute(); Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
scheduleAlarms(response.body()); scheduleAlarms(response.body());
return Result.success(); return Result.success();
} else { } else {
// Ved klientfeil (4xx), stopp retry-loop if (response.code() >= 400 && response.code() < 500) return Result.failure();
if (response.code() >= 400 && response.code() < 500) {
Log.w(TAG, "Kunne ikke hente kalender. Kode: " + response.code());
return Result.failure();
}
return Result.retry(); return Result.retry();
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Nettverksfeil under kalendersjekk", e);
return Result.retry(); return Result.retry();
} }
} }
@ -49,18 +45,15 @@ public class NotificationWorker extends Worker {
private void scheduleAlarms(List<CalendarEvent> events) { private void scheduleAlarms(List<CalendarEvent> events) {
Context context = getApplicationContext(); Context context = getApplicationContext();
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) return;
if (!alarmManager.canScheduleExactAlarms()) {
return; // Mangler rettigheter, kan ikke sette alarm
}
}
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
long catchUpWindow = now - (30 * 60 * 1000L); // 30 min bakover long catchUpWindow = now - (30 * 60 * 1000L);
long futureWindow = now + (24 * 60 * 60 * 1000L); // 24 timer fremover long futureWindow = now + (30 * 24 * 60 * 60 * 1000L); // 30 dager frem
for (CalendarEvent event : events) { for (CalendarEvent event : events) {
try { try {
@ -72,24 +65,28 @@ public class NotificationWorker extends Worker {
if (raw.length() > 19) raw = raw.substring(0, 19); if (raw.length() > 19) raw = raw.substring(0, 19);
eventDate = isoFormat.parse(raw); eventDate = isoFormat.parse(raw);
} }
if (eventDate == null) continue; if (eventDate == null) continue;
int minutesBefore = event.getReminderMinutes(); // Loop gjennom alle varsler for denne hendelsen
if (minutesBefore <= 0) continue; for (int minutesBefore : event.getReminders()) {
if (minutesBefore < 0) continue; // 0 betyr "ved start", negative ignoreres
long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L); long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L);
// Sjekk om alarmen er innenfor tidsvinduet // Unik nøkkel for denne alarmen: EventID + Tidspunkt
if (triggerTime > catchUpWindow && triggerTime < futureWindow) { String alarmKey = "alarm_" + event.getId() + "_" + triggerTime;
// Catch-up: Hvis tiden har passert, fyr av umiddelbart // Sjekk om vi allerede har fyrt denne alarmen
if (triggerTime < now) { if (prefs.getBoolean(alarmKey, false)) {
triggerTime = now + 1000; continue; // Allerede håndtert
} }
int alarmId = (event.getTitle() + event.getRawDate()).hashCode(); 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 intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("TITLE", event.getTitle()); intent.putExtra("TITLE", event.getTitle());
String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate); String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate);
@ -97,10 +94,7 @@ public class NotificationWorker extends Worker {
intent.putExtra("ID", alarmId); intent.putExtra("ID", alarmId);
PendingIntent pendingIntent = PendingIntent.getBroadcast( PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
alarmId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
); );
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -109,13 +103,16 @@ public class NotificationWorker extends Worker {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
} }
// Logger kun når en alarm faktisk blir satt/oppdatert // Marker som satt
Log.i(TAG, "Alarm satt for: " + event.getTitle() + " (" + new Date(triggerTime) + ")"); prefs.edit().putBoolean(alarmKey, true).apply();
Log.i(TAG, "Satt alarm: " + event.getTitle() + " (" + minutesBefore + "m før)");
}
}
} catch (Exception e) {
Log.e(TAG, "Feil", e);
}
} }
} catch (Exception e) { // Rensk opp gamle nøkler (valgfritt, for å spare plass over tid)
Log.e(TAG, "Feil ved behandling av event", e);
}
}
} }
} }

View file

@ -68,4 +68,11 @@ public interface WordPressApiService {
@GET("wp-json/kbs/v1/lookup-id") @GET("wp-json/kbs/v1/lookup-id")
Call<JsonObject> lookupPageId(@Query("url") String url); Call<JsonObject> lookupPageId(@Query("url") String url);
@POST("wp-json/kbs/v1/calendar/update")
Call<JsonElement> updateCalendarEvent(@Body CreateEventRequest request);
@POST("wp-json/kbs/v1/calendar/delete")
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type
} }

View file

@ -6,7 +6,6 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="24dp" android:padding="24dp"
android:background="@android:color/white"> android:background="@android:color/white">
<TextView <TextView
android:id="@+id/sheet_title" android:id="@+id/sheet_title"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -45,15 +44,43 @@
android:text="Beskrivelse..." android:text="Beskrivelse..."
android:textSize="14sp" android:textSize="14sp"
android:textColor="#555" android:textColor="#555"
android:layout_marginBottom="32dp"/> android:layout_marginBottom="24dp"/>
<!-- ADMIN KNAPPER (Vises kun for admin) -->
<LinearLayout
android:id="@+id/layout_admin_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Slett"
android:backgroundTint="#D32F2F"
android:textColor="#FFF"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btn_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Endre"
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="#FFF"
android:layout_marginStart="8dp"/>
</LinearLayout>
<!-- Gammel knapp (skjult) -->
<Button <Button
android:id="@+id/btn_add_to_calendar" android:id="@+id/btn_add_to_calendar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Lagre i min kalender / Varsle meg" android:visibility="gone"/>
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="@color/white"
android:padding="12dp"/>
</LinearLayout> </LinearLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#FFFFFF"> android:background="#FFFFFF">
@ -20,6 +21,7 @@
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:textColor="#333"/> android:textColor="#333"/>
<!-- TITTEL -->
<EditText <EditText
android:id="@+id/et_title" android:id="@+id/et_title"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -30,6 +32,7 @@
android:background="@android:drawable/edit_text" android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- BESKRIVELSE -->
<EditText <EditText
android:id="@+id/et_desc" android:id="@+id/et_desc"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -42,6 +45,7 @@
android:background="@android:drawable/edit_text" android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- STED -->
<EditText <EditText
android:id="@+id/et_location" android:id="@+id/et_location"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -52,6 +56,22 @@
android:background="@android:drawable/edit_text" android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- KALENDER VALG -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Velg Kalender:"
android:textSize="14sp"
android:textColor="#666"/>
<Spinner
android:id="@+id/spinner_calendar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="12dp"/>
<!-- HELE DAGEN -->
<Switch <Switch
android:id="@+id/switch_all_day" android:id="@+id/switch_all_day"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -59,7 +79,7 @@
android:text="Hele dagen" android:text="Hele dagen"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<!-- Start Dato/Tid --> <!-- START DATO/TID -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -83,7 +103,7 @@
android:layout_marginStart="4dp"/> android:layout_marginStart="4dp"/>
</LinearLayout> </LinearLayout>
<!-- Slutt Dato/Tid --> <!-- SLUTT DATO/TID -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -116,20 +136,7 @@
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="24dp"/> android:layout_marginBottom="24dp"/>
<TextView <!-- GJENTAKELSE -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Velg Kalender:"
android:textSize="14sp"
android:textColor="#666"/>
<Spinner
android:id="@+id/spinner_calendar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="12dp"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -144,19 +151,31 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:padding="12dp"/> android:padding="12dp"/>
<!-- NYTT: VARSLING MED CHIPS (MULTIVALG) -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Varsling:" android:text="Varsling (Velg en eller flere):"
android:textSize="14sp" android:textSize="14sp"
android:textColor="#666"/> android:textColor="#666"
android:layout_marginBottom="8dp"/>
<Spinner <HorizontalScrollView
android:id="@+id/spinner_reminder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="32dp" android:scrollbars="none"
android:padding="12dp"/> android:layout_marginBottom="32dp">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_reminders"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true"
app:selectionRequired="false">
<!-- Chips legges til programmatisk i Java -->
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<Button <Button
android:id="@+id/btn_save_event" android:id="@+id/btn_save_event"
@ -167,4 +186,5 @@
android:textColor="#FFFFFF"/> android:textColor="#FFFFFF"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>