diff --git a/app/src/main/java/com/kbs/kbsintranett/AddTaskBottomSheet.java b/app/src/main/java/com/kbs/kbsintranett/AddTaskBottomSheet.java index 4f173bf..5c946e2 100644 --- a/app/src/main/java/com/kbs/kbsintranett/AddTaskBottomSheet.java +++ b/app/src/main/java/com/kbs/kbsintranett/AddTaskBottomSheet.java @@ -30,10 +30,12 @@ import retrofit2.Response; public class AddTaskBottomSheet extends BottomSheetDialogFragment { private EditText etTitle, etDesc; - private Button btnDate, btnUsers, btnSave; + private Button btnDate, btnUsers, btnSave, btnClearDate; // NYTT private TextView txtSheetTitle, txtDatePreview, txtUsersPreview; private Calendar dueDate = Calendar.getInstance(); + private boolean hasDate = true; // NYTT + private List filteredUsers = new ArrayList<>(); private List selectedUsers = new ArrayList<>(); private TaskItem taskToEdit = null; @@ -77,6 +79,7 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { etTitle = v.findViewById(R.id.et_task_title); etDesc = v.findViewById(R.id.et_task_desc); btnDate = v.findViewById(R.id.btn_task_date); + btnClearDate = v.findViewById(R.id.btn_clear_date); // NYTT btnUsers = v.findViewById(R.id.btn_task_users); btnSave = v.findViewById(R.id.btn_save_task); txtDatePreview = v.findViewById(R.id.txt_date_preview); @@ -86,10 +89,17 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { txtSheetTitle.setText("Rediger Oppgave"); etTitle.setText(taskToEdit.getTitle()); etDesc.setText(taskToEdit.getDescription()); - dueDate.setTimeInMillis(taskToEdit.getDueDate()); + + if (taskToEdit.getDueDate() > 0) { + dueDate.setTimeInMillis(taskToEdit.getDueDate()); + hasDate = true; + } else { + hasDate = false; + } btnSave.setText("Oppdater Oppgave"); } else { dueDate.add(Calendar.DAY_OF_MONTH, 1); + hasDate = true; } updateDatePreview(); @@ -97,10 +107,17 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { btnDate.setOnClickListener(view -> { new DatePickerDialog(getContext(), (d, y, m, day) -> { dueDate.set(y, m, day); + hasDate = true; updateDatePreview(); }, dueDate.get(Calendar.YEAR), dueDate.get(Calendar.MONTH), dueDate.get(Calendar.DAY_OF_MONTH)).show(); }); + // NYTT: Knapp for å fjerne frist + btnClearDate.setOnClickListener(v1 -> { + hasDate = false; + updateDatePreview(); + }); + btnUsers.setOnClickListener(view -> showUserSelectionDialog()); btnSave.setOnClickListener(view -> saveTask()); @@ -114,7 +131,6 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { - // BRUKER HJELPEKLASSEN HER: filteredUsers = UserFilterHelper.getFilteredUsers(response.body()); if (taskToEdit != null) { @@ -173,8 +189,14 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { } private void updateDatePreview() { - SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); - txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime())); + if (hasDate) { + SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); + txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime())); + btnClearDate.setVisibility(View.VISIBLE); + } else { + txtDatePreview.setText("Ingen frist"); + btnClearDate.setVisibility(View.GONE); + } } private void saveTask() { @@ -184,6 +206,8 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { return; } + long finalDueDate = hasDate ? dueDate.getTimeInMillis() : 0; + if (taskToEdit != null) { Map oldStatus = taskToEdit.getAssigneeStatus(); taskToEdit.getAssigneeStatus().clear(); @@ -202,10 +226,10 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment { } taskToEdit.setTitle(title); taskToEdit.setDescription(etDesc.getText().toString()); - taskToEdit.setDueDate(dueDate.getTimeInMillis()); + taskToEdit.setDueDate(finalDueDate); if (listener != null) listener.onTaskUpdated(taskToEdit); } else { - TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), dueDate.getTimeInMillis()); + TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), finalDueDate); if (selectedUsers.isEmpty()) { newTask.addAssignee(UserManager.getInstance().getUserEmail()); } else { diff --git a/app/src/main/java/com/kbs/kbsintranett/CacheManager.java b/app/src/main/java/com/kbs/kbsintranett/CacheManager.java index 2ebfa21..2117e02 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CacheManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/CacheManager.java @@ -1,6 +1,7 @@ package com.kbs.kbsintranett; import android.content.Context; +import android.content.SharedPreferences; // NYTT import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -17,13 +18,18 @@ public class CacheManager { private static final String FILE_CALENDAR = "cache_calendar.json"; private static final String FILE_NEWS = "cache_news.json"; private static final String FILE_HANDBOOK = "cache_handbook.json"; + private static final String FILE_TASKS = "cache_tasks.json"; private static final String TAG = "CacheManager"; private static final Gson gson = new Gson(); + // NYTT: SharedPreferences for tidsstempler + private static final String PREFS_CACHE = "kbs_cache_prefs"; + // --- KALENDER --- public static void saveCalendarEvents(Context context, List events) { saveList(context, FILE_CALENDAR, events); + setLastFetchTime(context, "calendar"); // NYTT } public static List getCachedCalendarEvents(Context context) { @@ -35,6 +41,7 @@ public class CacheManager { // --- NYHETER --- public static void saveNewsPosts(Context context, List posts) { saveList(context, FILE_NEWS, posts); + setLastFetchTime(context, "news"); // NYTT } public static List getCachedNewsPosts(Context context) { @@ -46,6 +53,7 @@ public class CacheManager { // --- HÅNDBOK --- public static void saveHandbookItems(Context context, List items) { saveList(context, FILE_HANDBOOK, items); + // Håndboken endres sjelden, trenger kanskje ikke tidssjekk, men greit å ha } public static List getCachedHandbookItems(Context context) { @@ -54,7 +62,47 @@ public class CacheManager { return list != null ? list : new ArrayList<>(); } - // --- GENERISKE HJELPEMETODER --- + // --- OPPGAVER --- + public static void saveTasks(Context context, List tasks) { + saveList(context, FILE_TASKS, tasks); + setLastFetchTime(context, "tasks"); // NYTT + } + + public static List getTasks(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_TASKS, type); + return list != null ? list : new ArrayList<>(); + } + + // --- LOGIKK FOR TID --- + + private static void setLastFetchTime(Context context, String key) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); + prefs.edit().putLong(key + "_timestamp", System.currentTimeMillis()).apply(); + } + + /** + * Sjekker om cachen er gyldig. + * @param maxAgeMinutes Hvor gammel cachen kan være før vi henter på nytt (f.eks. 60 minutter). + * Hvis push fungerer, kan vi sette denne høyt! + */ + public static boolean isCacheValid(Context context, String key, int maxAgeMinutes) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); + long lastTime = prefs.getLong(key + "_timestamp", 0); + long diff = System.currentTimeMillis() - lastTime; + // Konverter minutter til millisekunder + long maxDiff = maxAgeMinutes * 60 * 1000L; + + return diff < maxDiff; + } + + // Metode for å tvinge oppdatering neste gang (brukes av FCM) + public static void invalidateCache(Context context, String key) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); + prefs.edit().remove(key + "_timestamp").apply(); + } + + // --- GENERISKE HJELPEMETODER (Uendret) --- private static void saveList(Context context, String filename, List list) { if (context == null || list == null) return; @@ -64,7 +112,6 @@ public class CacheManager { FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); fos.write(json.getBytes()); fos.close(); - Log.d(TAG, "Lagret cache til " + filename); } catch (Exception e) { Log.e(TAG, "Feil ved lagring av cache: " + filename, e); } @@ -92,15 +139,4 @@ public class CacheManager { return null; } } - private static final String FILE_TASKS = "cache_tasks.json"; - - public static void saveTasks(Context context, List tasks) { - saveList(context, FILE_TASKS, tasks); - } - - public static List getTasks(Context context) { - Type type = new TypeToken>() {}.getType(); - List list = loadList(context, FILE_TASKS, type); - return list != null ? list : new ArrayList<>(); - } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeAdapter.java b/app/src/main/java/com/kbs/kbsintranett/HomeAdapter.java index 0a519cd..a458168 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeAdapter.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeAdapter.java @@ -121,15 +121,21 @@ public class HomeAdapter extends RecyclerView.Adapter { TaskItem task = (TaskItem) item; TaskViewHolder vh = (TaskViewHolder) holder; vh.title.setText(task.getTitle()); - SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); - vh.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + + // FIKS: Vis "Ingen frist" hvis dato er 0 + if (task.getDueDate() > 0) { + SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); + vh.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + } else { + vh.date.setText("Ingen frist"); + } String myEmail = UserManager.getInstance().getUserEmail(); boolean myStatus = task.getParticipantStatus(myEmail); vh.checkBox.setChecked(myStatus); long now = System.currentTimeMillis(); - boolean isOverdue = task.getDueDate() < now && !task.isFullyCompleted() && !myStatus; + boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !myStatus; if (isOverdue) { vh.cardView.setCardBackgroundColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index e553a51..5948f8e 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -5,11 +5,11 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; -import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -33,6 +33,7 @@ import retrofit2.Response; public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickListener { + private static final String TAG = "HomeFragment"; private RecyclerView recyclerView; private HomeAdapter adapter; private ProgressBar mainProgressBar; @@ -41,8 +42,14 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis private List currentEvents = new ArrayList<>(); private List currentNews = new ArrayList<>(); private List currentTasks = new ArrayList<>(); + private int activeNetworkCalls = 0; + private Handler timeoutHandler = new Handler(Looper.getMainLooper()); + private Runnable timeoutRunnable = this::forceStopLoading; + + private static final int CACHE_TTL_MINUTES = 60; + private ActivityResultLauncher requestPermissionLauncher; @Override @@ -50,7 +57,7 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis super.onCreate(savedInstanceState); requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), - isGranted -> refreshData() + isGranted -> refreshData(true) ); } @@ -69,22 +76,82 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis recyclerView = view.findViewById(R.id.main_recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - swipeRefreshLayout.setOnRefreshListener(this::refreshData); - refreshData(); + swipeRefreshLayout.setOnRefreshListener(() -> refreshData(true)); + + refreshData(false); } - private void refreshData() { + @Override + public void onDestroyView() { + super.onDestroyView(); + timeoutHandler.removeCallbacks(timeoutRunnable); + } + + private void refreshData(boolean forceNetwork) { if (activeNetworkCalls > 0) return; + + boolean useCacheCalendar = !forceNetwork && CacheManager.isCacheValid(getContext(), "calendar", CACHE_TTL_MINUTES); + boolean useCacheNews = !forceNetwork && CacheManager.isCacheValid(getContext(), "news", CACHE_TTL_MINUTES); + boolean useCacheTasks = !forceNetwork && CacheManager.isCacheValid(getContext(), "tasks", CACHE_TTL_MINUTES); + + // Hvis alt er i cache, last direkte + if (useCacheCalendar && useCacheNews && useCacheTasks) { + loadAllFromCache(); + return; + } + activeNetworkCalls = 3; if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE); - fetchCalendarData(); - fetchNewsData(); - fetchTaskData(); + timeoutHandler.removeCallbacks(timeoutRunnable); + timeoutHandler.postDelayed(timeoutRunnable, 10000); + + fetchCalendarData(useCacheCalendar); + fetchNewsData(useCacheNews); + fetchTaskData(useCacheTasks); } - private void fetchCalendarData() { + private void forceStopLoading() { + if (activeNetworkCalls > 0) { + activeNetworkCalls = 0; + if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + buildAndDisplayList(); + } + } + + private void loadAllFromCache() { + if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE); + + new Thread(() -> { + List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); + new Handler(Looper.getMainLooper()).post(() -> { + if (!isAdded()) return; + + List cachedApi = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : cachedApi) CalendarManager.formatEventForUI(e); + currentEvents = CalendarManager.mergeAndSort(cachedApi, deviceEvents); + + currentNews = CacheManager.getCachedNewsPosts(getContext()); + formatNewsDates(currentNews); + + currentTasks = CacheManager.getTasks(getContext()); + + // FIKS: Hvis cache er tom, prøv nettverk likevel for å unngå tom skjerm + if (currentTasks.isEmpty() && !CacheManager.isCacheValid(getContext(), "tasks", CACHE_TTL_MINUTES)) { + fetchTaskData(false); + } + + if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + + buildAndDisplayList(); + }); + }).start(); + } + + private void fetchCalendarData(boolean useCache) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); currentEvents.clear(); @@ -93,74 +160,113 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis } new Thread(() -> { - List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); - new Handler(Looper.getMainLooper()).post(() -> { - RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - List apiEvents = new ArrayList<>(); - if (response.isSuccessful() && response.body() != null) { - apiEvents = response.body(); - CacheManager.saveCalendarEvents(getContext(), apiEvents); - } else { - apiEvents = CacheManager.getCachedCalendarEvents(getContext()); - } - for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); - currentEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - checkLoadingComplete(); - } - @Override - public void onFailure(Call> call, Throwable t) { + try { + if (getContext() == null) { + checkLoadingCompleteAsync(); + return; + } + + List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); + + new Handler(Looper.getMainLooper()).post(() -> { + if (!isAdded()) return; + + if (useCache) { List cached = CacheManager.getCachedCalendarEvents(getContext()); for (CalendarEvent e : cached) CalendarManager.formatEventForUI(e); currentEvents = CalendarManager.mergeAndSort(cached, deviceEvents); checkLoadingComplete(); + } else { + RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + List apiEvents = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null) { + apiEvents = response.body(); + CacheManager.saveCalendarEvents(getContext(), apiEvents); + } else { + apiEvents = CacheManager.getCachedCalendarEvents(getContext()); + } + for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); + currentEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); + checkLoadingComplete(); + } + @Override + public void onFailure(Call> call, Throwable t) { + List cached = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : cached) CalendarManager.formatEventForUI(e); + currentEvents = CalendarManager.mergeAndSort(cached, deviceEvents); + checkLoadingComplete(); + } + }); } }); - }); + } catch (Exception e) { + checkLoadingCompleteAsync(); + } }).start(); } - private void fetchNewsData() { - RetrofitClient.getApiService().getPosts().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - currentNews = response.body(); - CacheManager.saveNewsPosts(getContext(), currentNews); - } else { - currentNews = CacheManager.getCachedNewsPosts(getContext()); + private void fetchNewsData(boolean useCache) { + if (useCache) { + currentNews = CacheManager.getCachedNewsPosts(getContext()); + formatNewsDates(currentNews); + checkLoadingComplete(); + } else { + RetrofitClient.getApiService().getPosts().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + currentNews = response.body(); + CacheManager.saveNewsPosts(getContext(), currentNews); + } else { + currentNews = CacheManager.getCachedNewsPosts(getContext()); + } + formatNewsDates(currentNews); + checkLoadingComplete(); } - formatNewsDates(currentNews); - checkLoadingComplete(); - } - @Override - public void onFailure(Call> call, Throwable t) { - currentNews = CacheManager.getCachedNewsPosts(getContext()); - formatNewsDates(currentNews); - checkLoadingComplete(); - } - }); + @Override + public void onFailure(Call> call, Throwable t) { + currentNews = CacheManager.getCachedNewsPosts(getContext()); + formatNewsDates(currentNews); + checkLoadingComplete(); + } + }); + } } - private void fetchTaskData() { - RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - currentTasks = response.body(); - CacheManager.saveTasks(getContext(), currentTasks); - } else { - currentTasks = CacheManager.getTasks(getContext()); + private void fetchTaskData(boolean useCache) { + // FIKS: Hvis cache er valgt, men listen er tom, hent fra nett! + if (useCache) { + currentTasks = CacheManager.getTasks(getContext()); + if (currentTasks.isEmpty()) { + fetchTaskData(false); // Rekursivt kall men tvinger nettverk + return; + } + checkLoadingComplete(); + } else { + RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + currentTasks = response.body(); + CacheManager.saveTasks(getContext(), currentTasks); + } else { + currentTasks = CacheManager.getTasks(getContext()); + } + checkLoadingComplete(); } - checkLoadingComplete(); - } - @Override - public void onFailure(Call> call, Throwable t) { - currentTasks = CacheManager.getTasks(getContext()); - checkLoadingComplete(); - } - }); + @Override + public void onFailure(Call> call, Throwable t) { + currentTasks = CacheManager.getTasks(getContext()); + checkLoadingComplete(); + } + }); + } + } + + private void checkLoadingCompleteAsync() { + new Handler(Looper.getMainLooper()).post(this::checkLoadingComplete); } private void formatNewsDates(List posts) { @@ -174,17 +280,22 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis } } - private void checkLoadingComplete() { + private synchronized void checkLoadingComplete() { activeNetworkCalls--; if (activeNetworkCalls <= 0) { activeNetworkCalls = 0; + timeoutHandler.removeCallbacks(timeoutRunnable); + if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); - swipeRefreshLayout.setRefreshing(false); + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + buildAndDisplayList(); } } private void buildAndDisplayList() { + if (!isAdded()) return; + List items = new ArrayList<>(); String myEmail = UserManager.getInstance().getUserEmail(); @@ -201,23 +312,38 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis items.add(new HomeAdapter.SectionTitleItem("Mine oppgaver", HomeAdapter.SectionTitleItem.TYPE_TASKS)); List myActiveTasks = new ArrayList<>(); - for (TaskItem t : currentTasks) { - if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) { - myActiveTasks.add(t); + if (currentTasks != null) { + for (TaskItem t : currentTasks) { + if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) { + myActiveTasks.add(t); + } } } if (myActiveTasks.isEmpty()) { items.add(new HomeAdapter.EmptyTasksItem()); } else { - Collections.sort(myActiveTasks, (t1, t2) -> Long.compare(t1.getDueDate(), t2.getDueDate())); + // NY SORTERING: Med frist først, så uten frist (alfabetisk) + Collections.sort(myActiveTasks, (t1, t2) -> { + boolean t1HasDate = t1.getDueDate() > 0; + boolean t2HasDate = t2.getDueDate() > 0; + + if (t1HasDate && !t2HasDate) return -1; // t1 først + if (!t1HasDate && t2HasDate) return 1; // t2 først + if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle()); // Alfabetisk + + return Long.compare(t1.getDueDate(), t2.getDueDate()); // Dato + }); + for (int i = 0; i < Math.min(myActiveTasks.size(), 3); i++) { items.add(myActiveTasks.get(i)); } } items.add(new HomeAdapter.SectionTitleItem("Siste nytt", HomeAdapter.SectionTitleItem.TYPE_NEWS)); - items.addAll(currentNews); + if (currentNews != null) { + items.addAll(currentNews); + } adapter = new HomeAdapter(items, this); recyclerView.setAdapter(adapter); @@ -230,7 +356,7 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis @Override public void onCalendarItemClick(CalendarEvent event) { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.setOnEventChangeListener(this::refreshData); + sheet.setOnEventChangeListener(() -> refreshData(true)); sheet.show(getParentFragmentManager(), "CalendarDetails"); } diff --git a/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java b/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java index b928d10..22a664b 100644 --- a/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java +++ b/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java @@ -28,7 +28,6 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { @Override public void onNewToken(@NonNull String token) { super.onNewToken(token); - Log.d(TAG, "Ny FCM Token: " + token); AuthRepository.updateDeviceToken(token); } @@ -36,28 +35,32 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); - Log.d(TAG, "Melding mottatt fra: " + remoteMessage.getFrom()); - - // 1. Sjekk data payload (Bakgrunnsoppdatering) + // 1. DATA PAYLOAD (Bakgrunnsoppdatering) if (remoteMessage.getData().size() > 0) { String forceRefresh = remoteMessage.getData().get("force_refresh"); + String type = remoteMessage.getData().get("type"); // "tasks_update", "news_update", "calendar_update" if ("true".equalsIgnoreCase(forceRefresh)) { - Log.d(TAG, "Mottok 'force_refresh' - oppdaterer kalender og alarmer..."); - updateCalendarAndAlarms(); - } + Log.d(TAG, "Mottok force_refresh. Type: " + type); - // Hvis meldingen også har egne titler i data-feltet (valgfritt) - String title = remoteMessage.getData().get("title"); - String body = remoteMessage.getData().get("body"); - if (title != null && body != null) { - showNotification(title, body); + // Uansett hva det er, ugyldiggjør cachen slik at neste gang brukeren åpner appen, hentes ferske data. + if ("news_update".equals(type)) { + CacheManager.invalidateCache(getApplicationContext(), "news"); + } else if ("tasks_update".equals(type)) { + CacheManager.invalidateCache(getApplicationContext(), "tasks"); + updateTasksInBackground(); // Hent oppgaver med en gang i bakgrunnen + } else { + // Kalender eller generelt + CacheManager.invalidateCache(getApplicationContext(), "calendar"); + updateCalendarAndAlarms(); + } } } - // 2. Sjekk notification payload (Vises automatisk når app er i bakgrunn, men vi håndterer den her for forgrunn) + // 2. NOTIFICATION PAYLOAD (Synlig varsel - Nyheter etc) + // Android viser disse automatisk når appen er i bakgrunnen. + // Denne koden kjører hvis appen er i forgrunnen, eller hvis meldingen kun er "Data" og vi vil lage varsel manuelt. if (remoteMessage.getNotification() != null) { - Log.d(TAG, "Melding varsel body: " + remoteMessage.getNotification().getBody()); showNotification( remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody() @@ -66,24 +69,28 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { } private void updateCalendarAndAlarms() { - // Vi bruker Retrofit for å hente kalenderen på nytt RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { - // Lagre til cache først (god praksis) CacheManager.saveCalendarEvents(getApplicationContext(), response.body()); - - // Oppdater alarmer lokalt AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body()); - Log.d(TAG, "Kalender og alarmer oppdatert via Push."); } } + @Override public void onFailure(Call> call, Throwable t) {} + }); + } + private void updateTasksInBackground() { + // Henter oppgaver stille i bakgrunnen slik at de er klare når brukeren åpner appen + RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { @Override - public void onFailure(Call> call, Throwable t) { - Log.e(TAG, "Feil ved push-oppdatering av kalender", t); + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + CacheManager.saveTasks(getApplicationContext(), response.body()); + } } + @Override public void onFailure(Call> call, Throwable t) {} }); } @@ -99,10 +106,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { Intent tapIntent = new Intent(this, MainActivity.class); tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( - this, - 0, - tapIntent, - PendingIntent.FLAG_IMMUTABLE + this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) @@ -114,22 +118,15 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { .setContentIntent(pendingIntent) .setAutoCancel(true); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - notificationManager.notify((int) System.currentTimeMillis(), builder.build()); + NotificationManagerCompat.from(this).notify((int) System.currentTimeMillis(), builder.build()); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - "KBS Kalendervarsler", - NotificationManager.IMPORTANCE_HIGH + CHANNEL_ID, "KBS Varsler", NotificationManager.IMPORTANCE_HIGH ); - channel.setDescription("Varsler fra KBS Intranett"); - NotificationManager manager = getSystemService(NotificationManager.class); - if (manager != null) { - manager.createNotificationChannel(channel); - } + getSystemService(NotificationManager.class).createNotificationChannel(channel); } } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/TaskAdapter.java b/app/src/main/java/com/kbs/kbsintranett/TaskAdapter.java index 76d3d6b..2a2ea16 100644 --- a/app/src/main/java/com/kbs/kbsintranett/TaskAdapter.java +++ b/app/src/main/java/com/kbs/kbsintranett/TaskAdapter.java @@ -45,10 +45,13 @@ public class TaskAdapter extends RecyclerView.Adapter { TaskItem task = tasks.get(position); holder.title.setText(task.getTitle()); - SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); - holder.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + if (task.getDueDate() > 0) { + SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); + holder.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + } else { + holder.date.setText("Ingen frist"); + } - // Vis hvem som tildelte oppgaven hvis det ikke er meg selv (Nytt krav) if (task.getCreatedByEmail() != null && !task.getCreatedByEmail().equalsIgnoreCase(currentUserEmail)) { holder.creator.setText("Tildelt av: " + task.getCreatedByName()); holder.creator.setVisibility(View.VISIBLE); @@ -56,22 +59,40 @@ public class TaskAdapter extends RecyclerView.Adapter { holder.creator.setVisibility(View.GONE); } - boolean myStatus = task.getParticipantStatus(currentUserEmail); - holder.checkBox.setChecked(myStatus); + // SJEKK: Er jeg en deltaker? + boolean isParticipant = task.isUserParticipant(currentUserEmail); + + if (isParticipant) { + // Hvis jeg er deltaker: Vis checkbox og la meg endre MIN status + holder.checkBox.setVisibility(View.VISIBLE); + boolean myStatus = task.getParticipantStatus(currentUserEmail); + holder.checkBox.setOnCheckedChangeListener(null); // Hindre trigging ved resirkulering + holder.checkBox.setChecked(myStatus); + holder.checkBox.setOnClickListener(v -> listener.onStatusChanged(task, holder.checkBox.isChecked())); + } else { + // Hvis jeg IKKE er deltaker (f.eks. Admin som ser på "Alle"): + // Skjul checkboxen i listen. Admin må åpne detaljer for å endre andres status. + holder.checkBox.setVisibility(View.INVISIBLE); + holder.checkBox.setOnClickListener(null); + } long now = System.currentTimeMillis(); - boolean isOverdue = task.getDueDate() < now && !task.isFullyCompleted() && !myStatus; + // Sjekk overtid (kun hvis frist er satt) + // Er jeg ikke deltaker, sjekker vi om hele oppgaven er ferdig + boolean isDoneToCheck = isParticipant ? task.getParticipantStatus(currentUserEmail) : task.isFullyCompleted(); + boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !isDoneToCheck; if (isOverdue) { holder.cardView.setCardBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_logo_accent_red)); + SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); holder.date.setText("FORFALT: " + sdf.format(new Date(task.getDueDate()))); } else { holder.cardView.setCardBackgroundColor(Color.WHITE); holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_muted_blue_gray)); } - if (myStatus || task.isFullyCompleted()) { + if (isDoneToCheck || task.isFullyCompleted()) { holder.title.setPaintFlags(holder.title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); holder.title.setTextColor(Color.GRAY); holder.cardView.setCardBackgroundColor(Color.parseColor("#F5F5F5")); @@ -85,7 +106,6 @@ public class TaskAdapter extends RecyclerView.Adapter { for (Boolean b : task.getAssigneeStatus().values()) if (b) done++; holder.progress.setText("Fremdrift: " + done + "/" + total); - holder.checkBox.setOnClickListener(v -> listener.onStatusChanged(task, holder.checkBox.isChecked())); holder.itemView.setOnClickListener(v -> listener.onTaskClick(task)); } diff --git a/app/src/main/java/com/kbs/kbsintranett/TaskDetailsBottomSheet.java b/app/src/main/java/com/kbs/kbsintranett/TaskDetailsBottomSheet.java index 600fef2..7081402 100644 --- a/app/src/main/java/com/kbs/kbsintranett/TaskDetailsBottomSheet.java +++ b/app/src/main/java/com/kbs/kbsintranett/TaskDetailsBottomSheet.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; @@ -24,7 +25,7 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { public interface OnTaskChangeListener { void onTaskChanged(); void onTaskDeleted(TaskItem task); - void onEditRequested(TaskItem task); // NYTT + void onEditRequested(TaskItem task); } public TaskDetailsBottomSheet(TaskItem task, OnTaskChangeListener listener) { @@ -45,10 +46,15 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { LinearLayout ownerActions = v.findViewById(R.id.layout_owner_actions); Button btnDelete = v.findViewById(R.id.btn_delete_task); Button btnEdit = v.findViewById(R.id.btn_edit_task); + Button btnClose = v.findViewById(R.id.btn_close_details); // NYTT title.setText(task.getTitle()); - SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd. MMMM yyyy", Locale.getDefault()); - date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + if (task.getDueDate() > 0) { + SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd. MMMM yyyy", Locale.getDefault()); + date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); + } else { + date.setText("Ingen frist"); + } if (task.getDescription() != null && !task.getDescription().isEmpty()) { desc.setText(task.getDescription()); @@ -56,12 +62,47 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { } participantsContainer.removeAllViews(); + UserManager userManager = UserManager.getInstance(); + String myEmail = userManager.getUserEmail(); + + boolean canManageOthers = task.getCreatedByEmail().equalsIgnoreCase(myEmail) || userManager.isAdmin(); + for (Map.Entry entry : task.getAssigneeStatus().entrySet()) { - TextView t = new TextView(getContext()); - String status = entry.getValue() ? "✅ Fullført" : "⏳ Pågår"; - t.setText("• " + entry.getKey() + ": " + status); - t.setPadding(0, 4, 0, 4); - participantsContainer.addView(t); + String email = entry.getKey(); + boolean isDone = entry.getValue(); + + if (canManageOthers) { + CheckBox cb = new CheckBox(getContext()); + cb.setText(email + (isDone ? " (Fullført)" : "")); + cb.setChecked(isDone); + + if (isDone) cb.setTextColor(getResources().getColor(android.R.color.darker_gray)); + else cb.setTextColor(getResources().getColor(R.color.black)); + + cb.setOnCheckedChangeListener((buttonView, isChecked) -> { + task.setParticipantStatus(email, isChecked); + + cb.setText(email + (isChecked ? " (Fullført)" : "")); + if (isChecked) cb.setTextColor(getResources().getColor(android.R.color.darker_gray)); + else cb.setTextColor(getResources().getColor(R.color.black)); + + if (listener != null) listener.onTaskChanged(); + + // Sjekk om alle er ferdige, i så fall lukk vinduet + if (isChecked && areAllParticipantsFinished()) { + dismiss(); + } + }); + participantsContainer.addView(cb); + + } else { + TextView t = new TextView(getContext()); + String status = isDone ? "✅ Fullført" : "⏳ Pågår"; + t.setText("• " + email + ": " + status); + t.setPadding(0, 8, 0, 8); + t.setTextSize(14); + participantsContainer.addView(t); + } } switchNotify.setChecked(task.isNotificationsEnabled()); @@ -70,7 +111,7 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { if (listener != null) listener.onTaskChanged(); }); - if (task.getCreatedByEmail().equalsIgnoreCase(UserManager.getInstance().getUserEmail())) { + if (canManageOthers) { ownerActions.setVisibility(View.VISIBLE); btnDelete.setOnClickListener(view -> { if (listener != null) listener.onTaskDeleted(task); @@ -80,8 +121,20 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { if (listener != null) listener.onEditRequested(task); dismiss(); }); + } else { + ownerActions.setVisibility(View.GONE); } + // NYTT: Lukk-knapp funksjonalitet + btnClose.setOnClickListener(view -> dismiss()); + return v; } + + private boolean areAllParticipantsFinished() { + for (Boolean isDone : task.getAssigneeStatus().values()) { + if (!isDone) return false; + } + return true; + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/TasksFragment.java b/app/src/main/java/com/kbs/kbsintranett/TasksFragment.java index 093a4bb..147bf1a 100644 --- a/app/src/main/java/com/kbs/kbsintranett/TasksFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/TasksFragment.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,6 +28,7 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi private TaskAdapter adapter; private TabLayout tabLayout; private SwipeRefreshLayout swipeRefresh; + private CheckBox cbShowCompleted; private List allTasks = new ArrayList<>(); private String myEmail; @@ -44,6 +46,8 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi tabLayout = view.findViewById(R.id.task_tabs); recyclerView = view.findViewById(R.id.recycler_tasks); swipeRefresh = view.findViewById(R.id.swipe_refresh_tasks); + cbShowCompleted = view.findViewById(R.id.cb_show_completed); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); setupTabs(); @@ -67,14 +71,20 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi }); tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { - @Override public void onTabSelected(TabLayout.Tab tab) { filterAndDisplay(); } + @Override public void onTabSelected(TabLayout.Tab tab) { + updateFilterUI(tab.getPosition()); + filterAndDisplay(); + } @Override public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) {} }); + cbShowCompleted.setOnCheckedChangeListener((buttonView, isChecked) -> filterAndDisplay()); + swipeRefresh.setOnRefreshListener(this::fetchTasksFromServer); allTasks = CacheManager.getTasks(getContext()); + updateFilterUI(tabLayout.getSelectedTabPosition()); filterAndDisplay(); fetchTasksFromServer(); } @@ -89,6 +99,14 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi } } + private void updateFilterUI(int tabIndex) { + if (tabIndex == 3) { + cbShowCompleted.setVisibility(View.VISIBLE); + } else { + cbShowCompleted.setVisibility(View.GONE); + } + } + private void fetchTasksFromServer() { swipeRefresh.setRefreshing(true); RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { @@ -122,6 +140,27 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi if (!email.equalsIgnoreCase(myEmail)) { hasOtherParticipants = true; break; } } + // NYTT: Sjekk dynamisk om ALLE deltakerne har fullført oppgaven + boolean allParticipantsFinished = true; + if (t.getAssigneeStatus().isEmpty()) { + allParticipantsFinished = false; + } else { + for (Boolean isDone : t.getAssigneeStatus().values()) { + if (!isDone) { + allParticipantsFinished = false; + break; + } + } + } + + // En oppgave er "helt ferdig" hvis flagget er satt ELLER alle har krysset av + boolean effectivelyFinished = t.isFullyCompleted() || allParticipantsFinished; + + // Oppdater objektet slik at det lagres riktig neste gang (valgfritt men lurt) + if (effectivelyFinished && !t.isFullyCompleted()) { + t.setFullyCompleted(true); + } + switch (selectedTab) { case 0: // MINE if (isParticipant && !iHaveDoneIt && !t.isFullyCompleted()) filtered.add(t); @@ -132,13 +171,37 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi case 2: // TILDELT ANDRE if (isCreator && !t.isFullyCompleted() && hasOtherParticipants) filtered.add(t); break; - case 3: // ALLE - if (!t.isFullyCompleted()) filtered.add(t); + case 3: // ALLE (Admin) + if (cbShowCompleted.isChecked()) { + // Vis alt + filtered.add(t); + } else { + // Vis kun hvis IKKE ferdig + if (!effectivelyFinished) filtered.add(t); + } break; } } - if (selectedTab == 1) Collections.sort(filtered, (t1, t2) -> Long.compare(t2.getDueDate(), t1.getDueDate())); - else Collections.sort(filtered, (t1, t2) -> Long.compare(t1.getDueDate(), t2.getDueDate())); + + // SORTERING + Collections.sort(filtered, (t1, t2) -> { + boolean t1HasDate = t1.getDueDate() > 0; + boolean t2HasDate = t2.getDueDate() > 0; + + if (selectedTab == 1) { + if (t1HasDate && !t2HasDate) return -1; + if (!t1HasDate && t2HasDate) return 1; + if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle()); + return Long.compare(t2.getDueDate(), t1.getDueDate()); + } + else { + if (t1HasDate && !t2HasDate) return -1; + if (!t1HasDate && t2HasDate) return 1; + if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle()); + return Long.compare(t1.getDueDate(), t2.getDueDate()); + } + }); + adapter = new TaskAdapter(filtered, this); recyclerView.setAdapter(adapter); } @@ -155,7 +218,7 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi AddTaskBottomSheet editDialog = new AddTaskBottomSheet(); editDialog.setTaskToEdit(taskToEdit); editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() { - @Override public void onTaskAdded(TaskItem task) {} // Ikke i bruk her + @Override public void onTaskAdded(TaskItem task) {} @Override public void onTaskUpdated(TaskItem task) { saveAndSync(); } @@ -169,19 +232,14 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi @Override public void onStatusChanged(TaskItem task, boolean isDone) { task.setParticipantStatus(myEmail, isDone); - boolean hasOthers = false; - for (String email : task.getAssigneeStatus().keySet()) { - if (!email.equalsIgnoreCase(myEmail)) { hasOthers = true; break; } - } - if (task.getCreatedByEmail().equalsIgnoreCase(myEmail) && isDone && !hasOthers) { - task.setFullyCompleted(true); - } + // Her trenger vi ikke logikk for "hasOthers" osv, fordi saveAndSync() + // kaller filterAndDisplay() som nå regner ut status dynamisk. saveAndSync(); } private void saveAndSync() { CacheManager.saveTasks(getContext(), allTasks); - filterAndDisplay(); + filterAndDisplay(); // Oppdaterer visningen umiddelbart RetrofitClient.getApiService().syncTasks(allTasks).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) {} @Override public void onFailure(Call call, Throwable t) {} diff --git a/app/src/main/res/layout/bottom_sheet_add_task.xml b/app/src/main/res/layout/bottom_sheet_add_task.xml index a7526df..f14006c 100644 --- a/app/src/main/res/layout/bottom_sheet_add_task.xml +++ b/app/src/main/res/layout/bottom_sheet_add_task.xml @@ -45,18 +45,31 @@ android:orientation="horizontal" android:gravity="center_vertical" android:layout_marginBottom="12dp"> +