Før endringer av strømforbruket V2

This commit is contained in:
ErolHaagenrud 2026-01-08 14:00:42 +01:00
parent f7eeba2037
commit e89f7a3522
11 changed files with 519 additions and 164 deletions

View file

@ -30,10 +30,12 @@ import retrofit2.Response;
public class AddTaskBottomSheet extends BottomSheetDialogFragment { public class AddTaskBottomSheet extends BottomSheetDialogFragment {
private EditText etTitle, etDesc; private EditText etTitle, etDesc;
private Button btnDate, btnUsers, btnSave; private Button btnDate, btnUsers, btnSave, btnClearDate; // NYTT
private TextView txtSheetTitle, txtDatePreview, txtUsersPreview; private TextView txtSheetTitle, txtDatePreview, txtUsersPreview;
private Calendar dueDate = Calendar.getInstance(); private Calendar dueDate = Calendar.getInstance();
private boolean hasDate = true; // NYTT
private List<User> filteredUsers = new ArrayList<>(); private List<User> filteredUsers = new ArrayList<>();
private List<User> selectedUsers = new ArrayList<>(); private List<User> selectedUsers = new ArrayList<>();
private TaskItem taskToEdit = null; private TaskItem taskToEdit = null;
@ -77,6 +79,7 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
etTitle = v.findViewById(R.id.et_task_title); etTitle = v.findViewById(R.id.et_task_title);
etDesc = v.findViewById(R.id.et_task_desc); etDesc = v.findViewById(R.id.et_task_desc);
btnDate = v.findViewById(R.id.btn_task_date); 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); btnUsers = v.findViewById(R.id.btn_task_users);
btnSave = v.findViewById(R.id.btn_save_task); btnSave = v.findViewById(R.id.btn_save_task);
txtDatePreview = v.findViewById(R.id.txt_date_preview); txtDatePreview = v.findViewById(R.id.txt_date_preview);
@ -86,10 +89,17 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
txtSheetTitle.setText("Rediger Oppgave"); txtSheetTitle.setText("Rediger Oppgave");
etTitle.setText(taskToEdit.getTitle()); etTitle.setText(taskToEdit.getTitle());
etDesc.setText(taskToEdit.getDescription()); 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"); btnSave.setText("Oppdater Oppgave");
} else { } else {
dueDate.add(Calendar.DAY_OF_MONTH, 1); dueDate.add(Calendar.DAY_OF_MONTH, 1);
hasDate = true;
} }
updateDatePreview(); updateDatePreview();
@ -97,10 +107,17 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
btnDate.setOnClickListener(view -> { btnDate.setOnClickListener(view -> {
new DatePickerDialog(getContext(), (d, y, m, day) -> { new DatePickerDialog(getContext(), (d, y, m, day) -> {
dueDate.set(y, m, day); dueDate.set(y, m, day);
hasDate = true;
updateDatePreview(); updateDatePreview();
}, dueDate.get(Calendar.YEAR), dueDate.get(Calendar.MONTH), dueDate.get(Calendar.DAY_OF_MONTH)).show(); }, 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()); btnUsers.setOnClickListener(view -> showUserSelectionDialog());
btnSave.setOnClickListener(view -> saveTask()); btnSave.setOnClickListener(view -> saveTask());
@ -114,7 +131,6 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
@Override @Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) { public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
// BRUKER HJELPEKLASSEN HER:
filteredUsers = UserFilterHelper.getFilteredUsers(response.body()); filteredUsers = UserFilterHelper.getFilteredUsers(response.body());
if (taskToEdit != null) { if (taskToEdit != null) {
@ -173,8 +189,14 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
} }
private void updateDatePreview() { private void updateDatePreview() {
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); if (hasDate) {
txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime())); 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() { private void saveTask() {
@ -184,6 +206,8 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
return; return;
} }
long finalDueDate = hasDate ? dueDate.getTimeInMillis() : 0;
if (taskToEdit != null) { if (taskToEdit != null) {
Map<String, Boolean> oldStatus = taskToEdit.getAssigneeStatus(); Map<String, Boolean> oldStatus = taskToEdit.getAssigneeStatus();
taskToEdit.getAssigneeStatus().clear(); taskToEdit.getAssigneeStatus().clear();
@ -202,10 +226,10 @@ public class AddTaskBottomSheet extends BottomSheetDialogFragment {
} }
taskToEdit.setTitle(title); taskToEdit.setTitle(title);
taskToEdit.setDescription(etDesc.getText().toString()); taskToEdit.setDescription(etDesc.getText().toString());
taskToEdit.setDueDate(dueDate.getTimeInMillis()); taskToEdit.setDueDate(finalDueDate);
if (listener != null) listener.onTaskUpdated(taskToEdit); if (listener != null) listener.onTaskUpdated(taskToEdit);
} else { } else {
TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), dueDate.getTimeInMillis()); TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), finalDueDate);
if (selectedUsers.isEmpty()) { if (selectedUsers.isEmpty()) {
newTask.addAssignee(UserManager.getInstance().getUserEmail()); newTask.addAssignee(UserManager.getInstance().getUserEmail());
} else { } else {

View file

@ -1,6 +1,7 @@
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; // NYTT
import android.util.Log; import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; 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_CALENDAR = "cache_calendar.json";
private static final String FILE_NEWS = "cache_news.json"; private static final String FILE_NEWS = "cache_news.json";
private static final String FILE_HANDBOOK = "cache_handbook.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 String TAG = "CacheManager";
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
// NYTT: SharedPreferences for tidsstempler
private static final String PREFS_CACHE = "kbs_cache_prefs";
// --- KALENDER --- // --- KALENDER ---
public static void saveCalendarEvents(Context context, List<CalendarEvent> events) { public static void saveCalendarEvents(Context context, List<CalendarEvent> events) {
saveList(context, FILE_CALENDAR, events); saveList(context, FILE_CALENDAR, events);
setLastFetchTime(context, "calendar"); // NYTT
} }
public static List<CalendarEvent> getCachedCalendarEvents(Context context) { public static List<CalendarEvent> getCachedCalendarEvents(Context context) {
@ -35,6 +41,7 @@ public class CacheManager {
// --- NYHETER --- // --- NYHETER ---
public static void saveNewsPosts(Context context, List<WpPost> posts) { public static void saveNewsPosts(Context context, List<WpPost> posts) {
saveList(context, FILE_NEWS, posts); saveList(context, FILE_NEWS, posts);
setLastFetchTime(context, "news"); // NYTT
} }
public static List<WpPost> getCachedNewsPosts(Context context) { public static List<WpPost> getCachedNewsPosts(Context context) {
@ -46,6 +53,7 @@ public class CacheManager {
// --- HÅNDBOK --- // --- HÅNDBOK ---
public static void saveHandbookItems(Context context, List<HandbookItem> items) { public static void saveHandbookItems(Context context, List<HandbookItem> items) {
saveList(context, FILE_HANDBOOK, items); saveList(context, FILE_HANDBOOK, items);
// Håndboken endres sjelden, trenger kanskje ikke tidssjekk, men greit å ha
} }
public static List<HandbookItem> getCachedHandbookItems(Context context) { public static List<HandbookItem> getCachedHandbookItems(Context context) {
@ -54,7 +62,47 @@ public class CacheManager {
return list != null ? list : new ArrayList<>(); return list != null ? list : new ArrayList<>();
} }
// --- GENERISKE HJELPEMETODER --- // --- OPPGAVER ---
public static void saveTasks(Context context, List<TaskItem> tasks) {
saveList(context, FILE_TASKS, tasks);
setLastFetchTime(context, "tasks"); // NYTT
}
public static List<TaskItem> getTasks(Context context) {
Type type = new TypeToken<List<TaskItem>>() {}.getType();
List<TaskItem> 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 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 <T> void saveList(Context context, String filename, List<T> list) { private static <T> void saveList(Context context, String filename, List<T> list) {
if (context == null || list == null) return; if (context == null || list == null) return;
@ -64,7 +112,6 @@ public class CacheManager {
FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(json.getBytes()); fos.write(json.getBytes());
fos.close(); fos.close();
Log.d(TAG, "Lagret cache til " + filename);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Feil ved lagring av cache: " + filename, e); Log.e(TAG, "Feil ved lagring av cache: " + filename, e);
} }
@ -92,15 +139,4 @@ public class CacheManager {
return null; return null;
} }
} }
private static final String FILE_TASKS = "cache_tasks.json";
public static void saveTasks(Context context, List<TaskItem> tasks) {
saveList(context, FILE_TASKS, tasks);
}
public static List<TaskItem> getTasks(Context context) {
Type type = new TypeToken<List<TaskItem>>() {}.getType();
List<TaskItem> list = loadList(context, FILE_TASKS, type);
return list != null ? list : new ArrayList<>();
}
} }

View file

@ -121,15 +121,21 @@ public class HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
TaskItem task = (TaskItem) item; TaskItem task = (TaskItem) item;
TaskViewHolder vh = (TaskViewHolder) holder; TaskViewHolder vh = (TaskViewHolder) holder;
vh.title.setText(task.getTitle()); 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(); String myEmail = UserManager.getInstance().getUserEmail();
boolean myStatus = task.getParticipantStatus(myEmail); boolean myStatus = task.getParticipantStatus(myEmail);
vh.checkBox.setChecked(myStatus); vh.checkBox.setChecked(myStatus);
long now = System.currentTimeMillis(); 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) { if (isOverdue) {
vh.cardView.setCardBackgroundColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); vh.cardView.setCardBackgroundColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_soft_light_pink_beige));

View file

@ -5,11 +5,11 @@ import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
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.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -33,6 +33,7 @@ import retrofit2.Response;
public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickListener { public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickListener {
private static final String TAG = "HomeFragment";
private RecyclerView recyclerView; private RecyclerView recyclerView;
private HomeAdapter adapter; private HomeAdapter adapter;
private ProgressBar mainProgressBar; private ProgressBar mainProgressBar;
@ -41,8 +42,14 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis
private List<CalendarEvent> currentEvents = new ArrayList<>(); private List<CalendarEvent> currentEvents = new ArrayList<>();
private List<WpPost> currentNews = new ArrayList<>(); private List<WpPost> currentNews = new ArrayList<>();
private List<TaskItem> currentTasks = new ArrayList<>(); private List<TaskItem> currentTasks = new ArrayList<>();
private int activeNetworkCalls = 0; 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<String> requestPermissionLauncher; private ActivityResultLauncher<String> requestPermissionLauncher;
@Override @Override
@ -50,7 +57,7 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
requestPermissionLauncher = registerForActivityResult( requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(), 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 = view.findViewById(R.id.main_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 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; 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; activeNetworkCalls = 3;
if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE); if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE);
fetchCalendarData(); timeoutHandler.removeCallbacks(timeoutRunnable);
fetchNewsData(); timeoutHandler.postDelayed(timeoutRunnable, 10000);
fetchTaskData();
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<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true);
new Handler(Looper.getMainLooper()).post(() -> {
if (!isAdded()) return;
List<CalendarEvent> 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) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR);
currentEvents.clear(); currentEvents.clear();
@ -93,74 +160,113 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis
} }
new Thread(() -> { new Thread(() -> {
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); try {
new Handler(Looper.getMainLooper()).post(() -> { if (getContext() == null) {
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { checkLoadingCompleteAsync();
@Override return;
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { }
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) { List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true);
apiEvents = response.body();
CacheManager.saveCalendarEvents(getContext(), apiEvents); new Handler(Looper.getMainLooper()).post(() -> {
} else { if (!isAdded()) return;
apiEvents = CacheManager.getCachedCalendarEvents(getContext());
} if (useCache) {
for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e);
currentEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
checkLoadingComplete();
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
List<CalendarEvent> cached = CacheManager.getCachedCalendarEvents(getContext()); List<CalendarEvent> cached = CacheManager.getCachedCalendarEvents(getContext());
for (CalendarEvent e : cached) CalendarManager.formatEventForUI(e); for (CalendarEvent e : cached) CalendarManager.formatEventForUI(e);
currentEvents = CalendarManager.mergeAndSort(cached, deviceEvents); currentEvents = CalendarManager.mergeAndSort(cached, deviceEvents);
checkLoadingComplete(); checkLoadingComplete();
} else {
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
List<CalendarEvent> 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<List<CalendarEvent>> call, Throwable t) {
List<CalendarEvent> cached = CacheManager.getCachedCalendarEvents(getContext());
for (CalendarEvent e : cached) CalendarManager.formatEventForUI(e);
currentEvents = CalendarManager.mergeAndSort(cached, deviceEvents);
checkLoadingComplete();
}
});
} }
}); });
}); } catch (Exception e) {
checkLoadingCompleteAsync();
}
}).start(); }).start();
} }
private void fetchNewsData() { private void fetchNewsData(boolean useCache) {
RetrofitClient.getApiService().getPosts().enqueue(new Callback<List<WpPost>>() { if (useCache) {
@Override currentNews = CacheManager.getCachedNewsPosts(getContext());
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) { formatNewsDates(currentNews);
if (response.isSuccessful() && response.body() != null) { checkLoadingComplete();
currentNews = response.body(); } else {
CacheManager.saveNewsPosts(getContext(), currentNews); RetrofitClient.getApiService().getPosts().enqueue(new Callback<List<WpPost>>() {
} else { @Override
currentNews = CacheManager.getCachedNewsPosts(getContext()); public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) {
if (response.isSuccessful() && response.body() != null) {
currentNews = response.body();
CacheManager.saveNewsPosts(getContext(), currentNews);
} else {
currentNews = CacheManager.getCachedNewsPosts(getContext());
}
formatNewsDates(currentNews);
checkLoadingComplete();
} }
formatNewsDates(currentNews); @Override
checkLoadingComplete(); public void onFailure(Call<List<WpPost>> call, Throwable t) {
} currentNews = CacheManager.getCachedNewsPosts(getContext());
@Override formatNewsDates(currentNews);
public void onFailure(Call<List<WpPost>> call, Throwable t) { checkLoadingComplete();
currentNews = CacheManager.getCachedNewsPosts(getContext()); }
formatNewsDates(currentNews); });
checkLoadingComplete(); }
}
});
} }
private void fetchTaskData() { private void fetchTaskData(boolean useCache) {
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() { // FIKS: Hvis cache er valgt, men listen er tom, hent fra nett!
@Override if (useCache) {
public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> response) { currentTasks = CacheManager.getTasks(getContext());
if (response.isSuccessful() && response.body() != null) { if (currentTasks.isEmpty()) {
currentTasks = response.body(); fetchTaskData(false); // Rekursivt kall men tvinger nettverk
CacheManager.saveTasks(getContext(), currentTasks); return;
} else { }
currentTasks = CacheManager.getTasks(getContext()); checkLoadingComplete();
} else {
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() {
@Override
public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> 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<List<TaskItem>> call, Throwable t) {
@Override currentTasks = CacheManager.getTasks(getContext());
public void onFailure(Call<List<TaskItem>> call, Throwable t) { checkLoadingComplete();
currentTasks = CacheManager.getTasks(getContext()); }
checkLoadingComplete(); });
} }
}); }
private void checkLoadingCompleteAsync() {
new Handler(Looper.getMainLooper()).post(this::checkLoadingComplete);
} }
private void formatNewsDates(List<WpPost> posts) { private void formatNewsDates(List<WpPost> posts) {
@ -174,17 +280,22 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis
} }
} }
private void checkLoadingComplete() { private synchronized void checkLoadingComplete() {
activeNetworkCalls--; activeNetworkCalls--;
if (activeNetworkCalls <= 0) { if (activeNetworkCalls <= 0) {
activeNetworkCalls = 0; activeNetworkCalls = 0;
timeoutHandler.removeCallbacks(timeoutRunnable);
if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false); if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false);
buildAndDisplayList(); buildAndDisplayList();
} }
} }
private void buildAndDisplayList() { private void buildAndDisplayList() {
if (!isAdded()) return;
List<Object> items = new ArrayList<>(); List<Object> items = new ArrayList<>();
String myEmail = UserManager.getInstance().getUserEmail(); 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)); items.add(new HomeAdapter.SectionTitleItem("Mine oppgaver", HomeAdapter.SectionTitleItem.TYPE_TASKS));
List<TaskItem> myActiveTasks = new ArrayList<>(); List<TaskItem> myActiveTasks = new ArrayList<>();
for (TaskItem t : currentTasks) { if (currentTasks != null) {
if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) { for (TaskItem t : currentTasks) {
myActiveTasks.add(t); if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) {
myActiveTasks.add(t);
}
} }
} }
if (myActiveTasks.isEmpty()) { if (myActiveTasks.isEmpty()) {
items.add(new HomeAdapter.EmptyTasksItem()); items.add(new HomeAdapter.EmptyTasksItem());
} else { } else {
Collections.sort(myActiveTasks, (t1, t2) -> Long.compare(t1.getDueDate(), t2.getDueDate())); // NY SORTERING: Med frist først, 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++) { for (int i = 0; i < Math.min(myActiveTasks.size(), 3); i++) {
items.add(myActiveTasks.get(i)); items.add(myActiveTasks.get(i));
} }
} }
items.add(new HomeAdapter.SectionTitleItem("Siste nytt", HomeAdapter.SectionTitleItem.TYPE_NEWS)); 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); adapter = new HomeAdapter(items, this);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
@ -230,7 +356,7 @@ public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickLis
@Override public void onCalendarItemClick(CalendarEvent event) { @Override public void onCalendarItemClick(CalendarEvent event) {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.setOnEventChangeListener(this::refreshData); sheet.setOnEventChangeListener(() -> refreshData(true));
sheet.show(getParentFragmentManager(), "CalendarDetails"); sheet.show(getParentFragmentManager(), "CalendarDetails");
} }

View file

@ -28,7 +28,6 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
@Override @Override
public void onNewToken(@NonNull String token) { public void onNewToken(@NonNull String token) {
super.onNewToken(token); super.onNewToken(token);
Log.d(TAG, "Ny FCM Token: " + token);
AuthRepository.updateDeviceToken(token); AuthRepository.updateDeviceToken(token);
} }
@ -36,28 +35,32 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage); super.onMessageReceived(remoteMessage);
Log.d(TAG, "Melding mottatt fra: " + remoteMessage.getFrom()); // 1. DATA PAYLOAD (Bakgrunnsoppdatering)
// 1. Sjekk data payload (Bakgrunnsoppdatering)
if (remoteMessage.getData().size() > 0) { if (remoteMessage.getData().size() > 0) {
String forceRefresh = remoteMessage.getData().get("force_refresh"); String forceRefresh = remoteMessage.getData().get("force_refresh");
String type = remoteMessage.getData().get("type"); // "tasks_update", "news_update", "calendar_update"
if ("true".equalsIgnoreCase(forceRefresh)) { if ("true".equalsIgnoreCase(forceRefresh)) {
Log.d(TAG, "Mottok 'force_refresh' - oppdaterer kalender og alarmer..."); Log.d(TAG, "Mottok force_refresh. Type: " + type);
updateCalendarAndAlarms();
}
// Hvis meldingen også har egne titler i data-feltet (valgfritt) // Uansett hva det er, ugyldiggjør cachen slik at neste gang brukeren åpner appen, hentes ferske data.
String title = remoteMessage.getData().get("title"); if ("news_update".equals(type)) {
String body = remoteMessage.getData().get("body"); CacheManager.invalidateCache(getApplicationContext(), "news");
if (title != null && body != null) { } else if ("tasks_update".equals(type)) {
showNotification(title, body); 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) { if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Melding varsel body: " + remoteMessage.getNotification().getBody());
showNotification( showNotification(
remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody() remoteMessage.getNotification().getBody()
@ -66,24 +69,28 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
} }
private void updateCalendarAndAlarms() { private void updateCalendarAndAlarms() {
// Vi bruker Retrofit for å hente kalenderen nytt
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override @Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
// Lagre til cache først (god praksis)
CacheManager.saveCalendarEvents(getApplicationContext(), response.body()); CacheManager.saveCalendarEvents(getApplicationContext(), response.body());
// Oppdater alarmer lokalt
AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body()); AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body());
Log.d(TAG, "Kalender og alarmer oppdatert via Push.");
} }
} }
@Override public void onFailure(Call<List<CalendarEvent>> 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<List<TaskItem>>() {
@Override @Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) { public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> response) {
Log.e(TAG, "Feil ved push-oppdatering av kalender", t); if (response.isSuccessful() && response.body() != null) {
CacheManager.saveTasks(getApplicationContext(), response.body());
}
} }
@Override public void onFailure(Call<List<TaskItem>> call, Throwable t) {}
}); });
} }
@ -99,10 +106,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
Intent tapIntent = new Intent(this, MainActivity.class); Intent tapIntent = new Intent(this, MainActivity.class);
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity( PendingIntent pendingIntent = PendingIntent.getActivity(
this, this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE
0,
tapIntent,
PendingIntent.FLAG_IMMUTABLE
); );
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
@ -114,22 +118,15 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true); .setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); NotificationManagerCompat.from(this).notify((int) System.currentTimeMillis(), builder.build());
notificationManager.notify((int) System.currentTimeMillis(), builder.build());
} }
private void createNotificationChannel() { private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel(
CHANNEL_ID, CHANNEL_ID, "KBS Varsler", NotificationManager.IMPORTANCE_HIGH
"KBS Kalendervarsler",
NotificationManager.IMPORTANCE_HIGH
); );
channel.setDescription("Varsler fra KBS Intranett"); getSystemService(NotificationManager.class).createNotificationChannel(channel);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
} }
} }
} }

View file

@ -45,10 +45,13 @@ public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
TaskItem task = tasks.get(position); TaskItem task = tasks.get(position);
holder.title.setText(task.getTitle()); holder.title.setText(task.getTitle());
SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); if (task.getDueDate() > 0) {
holder.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); 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)) { if (task.getCreatedByEmail() != null && !task.getCreatedByEmail().equalsIgnoreCase(currentUserEmail)) {
holder.creator.setText("Tildelt av: " + task.getCreatedByName()); holder.creator.setText("Tildelt av: " + task.getCreatedByName());
holder.creator.setVisibility(View.VISIBLE); holder.creator.setVisibility(View.VISIBLE);
@ -56,22 +59,40 @@ public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
holder.creator.setVisibility(View.GONE); holder.creator.setVisibility(View.GONE);
} }
boolean myStatus = task.getParticipantStatus(currentUserEmail); // SJEKK: Er jeg en deltaker?
holder.checkBox.setChecked(myStatus); 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 "Alle"):
// Skjul checkboxen i listen. Admin åpne detaljer for å endre andres status.
holder.checkBox.setVisibility(View.INVISIBLE);
holder.checkBox.setOnClickListener(null);
}
long now = System.currentTimeMillis(); 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) { if (isOverdue) {
holder.cardView.setCardBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); 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)); 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()))); holder.date.setText("FORFALT: " + sdf.format(new Date(task.getDueDate())));
} else { } else {
holder.cardView.setCardBackgroundColor(Color.WHITE); holder.cardView.setCardBackgroundColor(Color.WHITE);
holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_muted_blue_gray)); 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.setPaintFlags(holder.title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
holder.title.setTextColor(Color.GRAY); holder.title.setTextColor(Color.GRAY);
holder.cardView.setCardBackgroundColor(Color.parseColor("#F5F5F5")); holder.cardView.setCardBackgroundColor(Color.parseColor("#F5F5F5"));
@ -85,7 +106,6 @@ public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
for (Boolean b : task.getAssigneeStatus().values()) if (b) done++; for (Boolean b : task.getAssigneeStatus().values()) if (b) done++;
holder.progress.setText("Fremdrift: " + done + "/" + total); holder.progress.setText("Fremdrift: " + done + "/" + total);
holder.checkBox.setOnClickListener(v -> listener.onStatusChanged(task, holder.checkBox.isChecked()));
holder.itemView.setOnClickListener(v -> listener.onTaskClick(task)); holder.itemView.setOnClickListener(v -> listener.onTaskClick(task));
} }

View file

@ -5,6 +5,7 @@ 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.CheckBox;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -24,7 +25,7 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
public interface OnTaskChangeListener { public interface OnTaskChangeListener {
void onTaskChanged(); void onTaskChanged();
void onTaskDeleted(TaskItem task); void onTaskDeleted(TaskItem task);
void onEditRequested(TaskItem task); // NYTT void onEditRequested(TaskItem task);
} }
public TaskDetailsBottomSheet(TaskItem task, OnTaskChangeListener listener) { public TaskDetailsBottomSheet(TaskItem task, OnTaskChangeListener listener) {
@ -45,10 +46,15 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
LinearLayout ownerActions = v.findViewById(R.id.layout_owner_actions); LinearLayout ownerActions = v.findViewById(R.id.layout_owner_actions);
Button btnDelete = v.findViewById(R.id.btn_delete_task); Button btnDelete = v.findViewById(R.id.btn_delete_task);
Button btnEdit = v.findViewById(R.id.btn_edit_task); Button btnEdit = v.findViewById(R.id.btn_edit_task);
Button btnClose = v.findViewById(R.id.btn_close_details); // NYTT
title.setText(task.getTitle()); title.setText(task.getTitle());
SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd. MMMM yyyy", Locale.getDefault()); if (task.getDueDate() > 0) {
date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); 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()) { if (task.getDescription() != null && !task.getDescription().isEmpty()) {
desc.setText(task.getDescription()); desc.setText(task.getDescription());
@ -56,12 +62,47 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
} }
participantsContainer.removeAllViews(); participantsContainer.removeAllViews();
UserManager userManager = UserManager.getInstance();
String myEmail = userManager.getUserEmail();
boolean canManageOthers = task.getCreatedByEmail().equalsIgnoreCase(myEmail) || userManager.isAdmin();
for (Map.Entry<String, Boolean> entry : task.getAssigneeStatus().entrySet()) { for (Map.Entry<String, Boolean> entry : task.getAssigneeStatus().entrySet()) {
TextView t = new TextView(getContext()); String email = entry.getKey();
String status = entry.getValue() ? "✅ Fullført" : "⏳ Pågår"; boolean isDone = entry.getValue();
t.setText("" + entry.getKey() + ": " + status);
t.setPadding(0, 4, 0, 4); if (canManageOthers) {
participantsContainer.addView(t); 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 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()); switchNotify.setChecked(task.isNotificationsEnabled());
@ -70,7 +111,7 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
if (listener != null) listener.onTaskChanged(); if (listener != null) listener.onTaskChanged();
}); });
if (task.getCreatedByEmail().equalsIgnoreCase(UserManager.getInstance().getUserEmail())) { if (canManageOthers) {
ownerActions.setVisibility(View.VISIBLE); ownerActions.setVisibility(View.VISIBLE);
btnDelete.setOnClickListener(view -> { btnDelete.setOnClickListener(view -> {
if (listener != null) listener.onTaskDeleted(task); if (listener != null) listener.onTaskDeleted(task);
@ -80,8 +121,20 @@ public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
if (listener != null) listener.onEditRequested(task); if (listener != null) listener.onEditRequested(task);
dismiss(); dismiss();
}); });
} else {
ownerActions.setVisibility(View.GONE);
} }
// NYTT: Lukk-knapp funksjonalitet
btnClose.setOnClickListener(view -> dismiss());
return v; return v;
} }
private boolean areAllParticipantsFinished() {
for (Boolean isDone : task.getAssigneeStatus().values()) {
if (!isDone) return false;
}
return true;
}
} }

View file

@ -4,6 +4,7 @@ import android.os.Bundle;
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.CheckBox;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -27,6 +28,7 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
private TaskAdapter adapter; private TaskAdapter adapter;
private TabLayout tabLayout; private TabLayout tabLayout;
private SwipeRefreshLayout swipeRefresh; private SwipeRefreshLayout swipeRefresh;
private CheckBox cbShowCompleted;
private List<TaskItem> allTasks = new ArrayList<>(); private List<TaskItem> allTasks = new ArrayList<>();
private String myEmail; private String myEmail;
@ -44,6 +46,8 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
tabLayout = view.findViewById(R.id.task_tabs); tabLayout = view.findViewById(R.id.task_tabs);
recyclerView = view.findViewById(R.id.recycler_tasks); recyclerView = view.findViewById(R.id.recycler_tasks);
swipeRefresh = view.findViewById(R.id.swipe_refresh_tasks); swipeRefresh = view.findViewById(R.id.swipe_refresh_tasks);
cbShowCompleted = view.findViewById(R.id.cb_show_completed);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
setupTabs(); setupTabs();
@ -67,14 +71,20 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
}); });
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { 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 onTabUnselected(TabLayout.Tab tab) {}
@Override public void onTabReselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) {}
}); });
cbShowCompleted.setOnCheckedChangeListener((buttonView, isChecked) -> filterAndDisplay());
swipeRefresh.setOnRefreshListener(this::fetchTasksFromServer); swipeRefresh.setOnRefreshListener(this::fetchTasksFromServer);
allTasks = CacheManager.getTasks(getContext()); allTasks = CacheManager.getTasks(getContext());
updateFilterUI(tabLayout.getSelectedTabPosition());
filterAndDisplay(); filterAndDisplay();
fetchTasksFromServer(); 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() { private void fetchTasksFromServer() {
swipeRefresh.setRefreshing(true); swipeRefresh.setRefreshing(true);
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() { RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() {
@ -122,6 +140,27 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
if (!email.equalsIgnoreCase(myEmail)) { hasOtherParticipants = true; break; } 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) { switch (selectedTab) {
case 0: // MINE case 0: // MINE
if (isParticipant && !iHaveDoneIt && !t.isFullyCompleted()) filtered.add(t); if (isParticipant && !iHaveDoneIt && !t.isFullyCompleted()) filtered.add(t);
@ -132,13 +171,37 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
case 2: // TILDELT ANDRE case 2: // TILDELT ANDRE
if (isCreator && !t.isFullyCompleted() && hasOtherParticipants) filtered.add(t); if (isCreator && !t.isFullyCompleted() && hasOtherParticipants) filtered.add(t);
break; break;
case 3: // ALLE case 3: // ALLE (Admin)
if (!t.isFullyCompleted()) filtered.add(t); if (cbShowCompleted.isChecked()) {
// Vis alt
filtered.add(t);
} else {
// Vis kun hvis IKKE ferdig
if (!effectivelyFinished) filtered.add(t);
}
break; 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); adapter = new TaskAdapter(filtered, this);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
} }
@ -155,7 +218,7 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
AddTaskBottomSheet editDialog = new AddTaskBottomSheet(); AddTaskBottomSheet editDialog = new AddTaskBottomSheet();
editDialog.setTaskToEdit(taskToEdit); editDialog.setTaskToEdit(taskToEdit);
editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() { 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) { @Override public void onTaskUpdated(TaskItem task) {
saveAndSync(); saveAndSync();
} }
@ -169,19 +232,14 @@ public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickLi
@Override @Override
public void onStatusChanged(TaskItem task, boolean isDone) { public void onStatusChanged(TaskItem task, boolean isDone) {
task.setParticipantStatus(myEmail, isDone); task.setParticipantStatus(myEmail, isDone);
boolean hasOthers = false; // Her trenger vi ikke logikk for "hasOthers" osv, fordi saveAndSync()
for (String email : task.getAssigneeStatus().keySet()) { // kaller filterAndDisplay() som regner ut status dynamisk.
if (!email.equalsIgnoreCase(myEmail)) { hasOthers = true; break; }
}
if (task.getCreatedByEmail().equalsIgnoreCase(myEmail) && isDone && !hasOthers) {
task.setFullyCompleted(true);
}
saveAndSync(); saveAndSync();
} }
private void saveAndSync() { private void saveAndSync() {
CacheManager.saveTasks(getContext(), allTasks); CacheManager.saveTasks(getContext(), allTasks);
filterAndDisplay(); filterAndDisplay(); // Oppdaterer visningen umiddelbart
RetrofitClient.getApiService().syncTasks(allTasks).enqueue(new Callback<JsonElement>() { RetrofitClient.getApiService().syncTasks(allTasks).enqueue(new Callback<JsonElement>() {
@Override public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {} @Override public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {}
@Override public void onFailure(Call<JsonElement> call, Throwable t) {} @Override public void onFailure(Call<JsonElement> call, Throwable t) {}

View file

@ -45,18 +45,31 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_marginBottom="12dp"> android:layout_marginBottom="12dp">
<Button <Button
android:id="@+id/btn_task_date" android:id="@+id/btn_task_date"
style="@style/Widget.MaterialComponents.Button.TextButton" style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Sett frist" /> android:text="Sett frist" />
<TextView <TextView
android:id="@+id/txt_date_preview" android:id="@+id/txt_date_preview"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Frist: -" android:text="Frist: -"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
<!-- NYTT: Fjern frist knapp -->
<Button
android:id="@+id/btn_clear_date"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="X"
android:textColor="#999999"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View file

@ -61,7 +61,7 @@
android:id="@+id/switch_notifications" android:id="@+id/switch_notifications"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Varsle meg om denne oppgaven" android:text="Aktiver påminnelser til deltakere"
android:checked="true" android:checked="true"
android:layout_marginBottom="24dp"/> android:layout_marginBottom="24dp"/>
@ -70,6 +70,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:visibility="gone"> android:visibility="gone">
<Button <Button
@ -93,4 +94,13 @@
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
</LinearLayout> </LinearLayout>
<!-- NY LUKK KNAPP -->
<Button
android:id="@+id/btn_close_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Lukk / Ferdig"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:textColor="@color/kbs_muted_blue_gray"/>
</LinearLayout> </LinearLayout>

View file

@ -13,9 +13,21 @@
android:background="@color/white" android:background="@color/white"
app:tabSelectedTextColor="@color/kbs_logo_blue" app:tabSelectedTextColor="@color/kbs_logo_blue"
app:tabIndicatorColor="@color/kbs_logo_blue"> app:tabIndicatorColor="@color/kbs_logo_blue">
<!-- Faner legges til programmatisk i TasksFragment.java -->
</com.google.android.material.tabs.TabLayout> </com.google.android.material.tabs.TabLayout>
<!-- NY SJEKKBOKS: Vises kun under "Alle" -->
<CheckBox
android:id="@+id/cb_show_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Vis også fullførte oppgaver"
android:textColor="@color/kbs_muted_blue_gray"
android:textSize="12sp"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:layout_marginTop="4dp"
android:visibility="gone"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_tasks" android:id="@+id/swipe_refresh_tasks"
android:layout_width="match_parent" android:layout_width="match_parent"