diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93a976a..1521411 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,16 @@ android:resource="@xml/file_paths" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/AlarmReceiver.java b/app/src/main/java/com/kbs/kbsintranett/AlarmReceiver.java index 6811f6e..51ddf3f 100644 --- a/app/src/main/java/com/kbs/kbsintranett/AlarmReceiver.java +++ b/app/src/main/java/com/kbs/kbsintranett/AlarmReceiver.java @@ -12,6 +12,7 @@ import android.os.Build; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; // <-- Denne manglet public class AlarmReceiver extends BroadcastReceiver { @@ -48,7 +49,8 @@ public class AlarmReceiver extends BroadcastReceiver { ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_launcher_foreground) // Pass på at du har et ikon her, ellers bruk R.mipmap.ic_launcher + .setSmallIcon(R.drawable.ic_stat_kbs) + .setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue)) // Setter KBS-blå farge på ikonet .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) diff --git a/app/src/main/java/com/kbs/kbsintranett/CacheManager.java b/app/src/main/java/com/kbs/kbsintranett/CacheManager.java new file mode 100644 index 0000000..de254a6 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/CacheManager.java @@ -0,0 +1,95 @@ +package com.kbs.kbsintranett; + +import android.content.Context; +import android.util.Log; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +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 TAG = "CacheManager"; + private static final Gson gson = new Gson(); + + // --- KALENDER --- + public static void saveCalendarEvents(Context context, List events) { + saveList(context, FILE_CALENDAR, events); + } + + public static List getCachedCalendarEvents(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_CALENDAR, type); + return list != null ? list : new ArrayList<>(); + } + + // --- NYHETER --- + public static void saveNewsPosts(Context context, List posts) { + saveList(context, FILE_NEWS, posts); + } + + public static List getCachedNewsPosts(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_NEWS, type); + return list != null ? list : new ArrayList<>(); + } + + // --- HÅNDBOK --- + public static void saveHandbookItems(Context context, List items) { + saveList(context, FILE_HANDBOOK, items); + } + + public static List getCachedHandbookItems(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_HANDBOOK, type); + return list != null ? list : new ArrayList<>(); + } + + // --- GENERISKE HJELPEMETODER --- + + private static void saveList(Context context, String filename, List list) { + if (context == null || list == null) return; + new Thread(() -> { + try { + String json = gson.toJson(list); + 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); + } + }).start(); + } + + private static List loadList(Context context, String filename, Type type) { + if (context == null) return null; + File file = new File(context.getFilesDir(), filename); + if (!file.exists()) return null; + + try { + FileInputStream fis = context.openFileInput(filename); + InputStreamReader isr = new InputStreamReader(fis); + BufferedReader bufferedReader = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + sb.append(line); + } + fis.close(); + return gson.fromJson(sb.toString(), type); + } catch (Exception e) { + Log.e(TAG, "Feil ved lesing av cache: " + filename, e); + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index a871508..a4bdbee 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -11,6 +11,7 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -89,7 +90,6 @@ public class HomeFragment extends Fragment { btnCreateEvent.setVisibility(View.GONE); } - // KALENDER: Standard scrolling (slik at man kan scrolle i vinduet på 230dp) calendarRecycler = view.findViewById(R.id.recycler_calendar); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); @@ -105,7 +105,6 @@ public class HomeFragment extends Fragment { } } - // NYHETER: Nested scrolling disabled (skal vises i full høyde under kalenderen) newsRecycler = view.findViewById(R.id.recycler_news); newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); newsRecycler.setNestedScrollingEnabled(false); @@ -167,33 +166,21 @@ public class HomeFragment extends Fragment { List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { apiEvents = response.body(); + + CacheManager.saveCalendarEvents(getContext(), apiEvents); + for (CalendarEvent e : apiEvents) { CalendarManager.formatEventForUI(e); } - } - - List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - List upcomingEvents = new ArrayList<>(); - String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - - for (CalendarEvent e : merged) { - if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { - upcomingEvents.add(e); + } else { + apiEvents = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); + if (!apiEvents.isEmpty()) { + Toast.makeText(getContext(), "Server utilgjengelig. Viser lagret kalender.", Toast.LENGTH_SHORT).show(); } } - List topEvents = new ArrayList<>(); - // ENDRET: Grensen er satt tilbake til 5 - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.setOnEventChangeListener(HomeFragment.this::refreshData); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); - + updateCalendarUI(recyclerView, apiEvents, deviceEvents); checkLoadingComplete(); } @@ -201,23 +188,42 @@ public class HomeFragment extends Fragment { public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; - if (!deviceEvents.isEmpty()) { - List topEvents = new ArrayList<>(); - // ENDRET: Grensen er satt tilbake til 5 (Fallback) - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); - } else { - recyclerView.setAdapter(new CalendarAdapter(new ArrayList<>(), null)); + List cachedApiEvents = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : cachedApiEvents) CalendarManager.formatEventForUI(e); + + if (!cachedApiEvents.isEmpty()) { + Toast.makeText(getContext(), "Ingen nettverk. Viser lagret kalender.", Toast.LENGTH_SHORT).show(); } + updateCalendarUI(recyclerView, cachedApiEvents, deviceEvents); checkLoadingComplete(); } }); } + private void updateCalendarUI(RecyclerView recyclerView, List apiEvents, List deviceEvents) { + List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); + List upcomingEvents = new ArrayList<>(); + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + + for (CalendarEvent e : merged) { + if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { + upcomingEvents.add(e); + } + } + + List topEvents = new ArrayList<>(); + for(int i=0; i { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.setOnEventChangeListener(HomeFragment.this::refreshData); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + })); + } + private void fetchNewsFromWordpress(RecyclerView recyclerView) { WordPressApiService apiService = RetrofitClient.getApiService(); apiService.getPosts().enqueue(new Callback>() { @@ -225,39 +231,58 @@ public class HomeFragment extends Fragment { public void onResponse(Call> call, Response> response) { if (getContext() == null) return; + List postsToShow = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null) { - List wpPosts = response.body(); - SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); - targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - for (WpPost post : wpPosts) { - try { - Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); - } catch (Exception e) {} + postsToShow = response.body(); + CacheManager.saveNewsPosts(getContext(), postsToShow); + } else { + postsToShow = CacheManager.getCachedNewsPosts(getContext()); + if (!postsToShow.isEmpty()) { + Toast.makeText(getContext(), "Server utilgjengelig. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show(); } - - NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { - Bundle bundle = new Bundle(); - bundle.putSerializable("post_data", post); - Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); - }); - recyclerView.setAdapter(adapter); } + + updateNewsUI(recyclerView, postsToShow); checkLoadingComplete(); } @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; - recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); + + List cachedPosts = CacheManager.getCachedNewsPosts(getContext()); + if (!cachedPosts.isEmpty()) { + Toast.makeText(getContext(), "Ingen nettverk. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show(); + } + + updateNewsUI(recyclerView, cachedPosts); checkLoadingComplete(); } }); } + private void updateNewsUI(RecyclerView recyclerView, List posts) { + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + for (WpPost post : posts) { + try { + Date date = rawFormat.parse(post.date); + post.date = targetFormat.format(date); + } catch (Exception e) {} + } + + NewsAdapter adapter = new NewsAdapter(posts, post -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); + Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); + }); + recyclerView.setAdapter(adapter); + } + private void startNotificationWorker() { PeriodicWorkRequest notifRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) diff --git a/app/src/main/res/drawable-hdpi/ic_stat_kbs.png b/app/src/main/res/drawable-hdpi/ic_stat_kbs.png new file mode 100644 index 0000000..0aa6148 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_kbs.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_kbs.png b/app/src/main/res/drawable-mdpi/ic_stat_kbs.png new file mode 100644 index 0000000..789bb4b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_kbs.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_kbs.png b/app/src/main/res/drawable-xhdpi/ic_stat_kbs.png new file mode 100644 index 0000000..7143739 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_kbs.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_kbs.png b/app/src/main/res/drawable-xxhdpi/ic_stat_kbs.png new file mode 100644 index 0000000..e05de22 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_kbs.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_kbs.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_kbs.png new file mode 100644 index 0000000..819488a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_kbs.png differ diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index 13af76b..3bf6d6a 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -8,8 +8,11 @@ FILSTI: build.gradle.kts // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + // NY LINJE: Legg til Google Services plugin her + id("com.google.gms.google-services") version "4.4.2" apply false } + ============================================================ FILSTI: settings.gradle.kts ============================================================ @@ -43,6 +46,8 @@ FILSTI: app\build.gradle.kts ============================================================ plugins { alias(libs.plugins.android.application) + // NY LINJE: Aktiver Google Services plugin her + id("com.google.gms.google-services") } android { @@ -53,8 +58,8 @@ android { applicationId = "com.kbs.kbsintranett" minSdk = 28 targetSdk = 34 - versionCode = 1 - versionName = "1.0" + versionCode = 3 + versionName = "1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -106,8 +111,15 @@ dependencies { // Swipe Refresh Layout implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + + // NY LINJE: Firebase BOM (Bill of Materials) styrer versjoner + implementation(platform("com.google.firebase:firebase-bom:33.1.2")) + + // NY LINJE: (Valgfritt, men lurt for statistikk) + implementation("com.google.firebase:firebase-analytics") } + ============================================================ FILSTI: app\proguard-rules.pro ============================================================ @@ -227,6 +239,16 @@ FILSTI: app\src\main\AndroidManifest.xml android:resource="@xml/file_paths" /> + + + + + + @@ -248,6 +270,7 @@ import android.os.Build; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; // <-- Denne manglet public class AlarmReceiver extends BroadcastReceiver { @@ -284,7 +307,8 @@ public class AlarmReceiver extends BroadcastReceiver { ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_launcher_foreground) // Pass på at du har et ikon her, ellers bruk R.mipmap.ic_launcher + .setSmallIcon(R.drawable.ic_stat_kbs) + .setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue)) // Setter KBS-blå farge på ikonet .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -388,14 +412,116 @@ public class AuthRepository { } } +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\CacheManager.java +============================================================ +package com.kbs.kbsintranett; + +import android.content.Context; +import android.util.Log; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +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 TAG = "CacheManager"; + private static final Gson gson = new Gson(); + + // --- KALENDER --- + public static void saveCalendarEvents(Context context, List events) { + saveList(context, FILE_CALENDAR, events); + } + + public static List getCachedCalendarEvents(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_CALENDAR, type); + return list != null ? list : new ArrayList<>(); + } + + // --- NYHETER --- + public static void saveNewsPosts(Context context, List posts) { + saveList(context, FILE_NEWS, posts); + } + + public static List getCachedNewsPosts(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_NEWS, type); + return list != null ? list : new ArrayList<>(); + } + + // --- HÅNDBOK --- + public static void saveHandbookItems(Context context, List items) { + saveList(context, FILE_HANDBOOK, items); + } + + public static List getCachedHandbookItems(Context context) { + Type type = new TypeToken>() {}.getType(); + List list = loadList(context, FILE_HANDBOOK, type); + return list != null ? list : new ArrayList<>(); + } + + // --- GENERISKE HJELPEMETODER --- + + private static void saveList(Context context, String filename, List list) { + if (context == null || list == null) return; + new Thread(() -> { + try { + String json = gson.toJson(list); + 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); + } + }).start(); + } + + private static List loadList(Context context, String filename, Type type) { + if (context == null) return null; + File file = new File(context.getFilesDir(), filename); + if (!file.exists()) return null; + + try { + FileInputStream fis = context.openFileInput(filename); + InputStreamReader isr = new InputStreamReader(fis); + BufferedReader bufferedReader = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + sb.append(line); + } + fis.close(); + return gson.fromJson(sb.toString(), type); + } catch (Exception e) { + Log.e(TAG, "Feil ved lesing av cache: " + filename, e); + return null; + } + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarAdapter.java ============================================================ package com.kbs.kbsintranett; +import android.content.res.ColorStateList; +import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; @@ -410,7 +536,6 @@ public class CalendarAdapter extends RecyclerView.Adapter events, OnItemClickListener listener) { this.events = events; this.listener = listener; @@ -431,6 +556,15 @@ public class CalendarAdapter extends RecyclerView.Adapter { if (listener != null) { listener.onItemClick(event); @@ -445,6 +579,7 @@ public class CalendarAdapter extends RecyclerView.Adapter confirmDelete()); - - btnEdit.setOnClickListener(v -> { - // Send eventet videre til redigering - Bundle bundle = new Bundle(); - bundle.putSerializable("edit_event", event); - // Vi må navigere via parent fragmentets navController - NavHostFragment.findNavController(this).navigate(R.id.navigation_create_event, bundle); - dismiss(); - }); + if (canEdit) { + adminLayout.setVisibility(View.VISIBLE); + btnDelete.setOnClickListener(v -> confirmDelete()); + btnEdit.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("edit_event", event); + NavHostFragment.findNavController(this).navigate(R.id.navigation_create_event, bundle); + dismiss(); + }); + } else { + adminLayout.setVisibility(View.GONE); + } } return view; @@ -553,15 +709,8 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { } private void deleteEvent() { - // Vi må sende en CreateEventRequest med ID for å slette - // Vi trenger ikke fylle ut alt, bare ID og kalendertype - // Siden vi ikke vet nøyaktig hvilken kalender den kom fra (APIet gir ikke det), - // prøver vi "Felles" som default, eller prøver å slette fra ID. - // PHP-koden vi lagde støtter sletting basert på ID hvis vi sender riktig kalendertype. - // For nå antar vi "Felles" eller looper i backend. I V11.3 PHP scriptet sletter den basert på ID i valgt kalender. - CreateEventRequest req = new CreateEventRequest( - "", "", "", "", "", "Felles", new ArrayList<>(), false, "" + "", "", "", "", "", event.getCalendarName(), new ArrayList<>(), false, "" ); req.id = event.getId(); @@ -570,10 +719,15 @@ public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { public void onResponse(Call call, Response response) { if (response.isSuccessful()) { Toast.makeText(getContext(), "Slettet!", Toast.LENGTH_SHORT).show(); + + // VARSLE LISTEN OM AT NOE ER SLETTET + if (changeListener != null) { + changeListener.onEventChanged(); + } + dismiss(); - // Her burde vi ideelt sett oppdatert listen bak, men brukeren kan dra for å oppdatere } else { - Toast.makeText(getContext(), "Kunne ikke slette (Er det en felles-hendelse?)", Toast.LENGTH_LONG).show(); + Toast.makeText(getContext(), "Kunne ikke slette", Toast.LENGTH_LONG).show(); } } @@ -613,10 +767,16 @@ public class CalendarEvent implements Serializable { @SerializedName("location") private String location; - // V11.0: Liste av minutter (f.eks [15, 60]) @SerializedName("reminders") private List reminders = new ArrayList<>(); + // NYE FELTER V12.2 + @SerializedName("calendar_name") + private String calendarName; + + @SerializedName("calendar_color") + private String calendarColor; + // UI-hjelpefelter private String day; private String month; @@ -638,14 +798,18 @@ public class CalendarEvent implements Serializable { public String getDescription() { return description != null ? description : ""; } public String getLocation() { return location != null ? location : ""; } - // Henter listen. Hvis den er null (gamle data), returner tom liste. public List getReminders() { return reminders != null ? reminders : new ArrayList<>(); } -// --- KOMPATIBILITETS-METODER (For å fikse build-feil i CalendarManager og Worker) --- + // NYE GETTERS/SETTERS + public String getCalendarName() { return calendarName != null ? calendarName : "Ukjent"; } + public void setCalendarName(String name) { this.calendarName = name; } - // Brukes av CalendarManager.java for lokale events + public String getCalendarColor() { return calendarColor != null ? calendarColor : "#888888"; } + public void setCalendarColor(String color) { this.calendarColor = color; } + + // --- KOMPATIBILITETS-METODER --- public void setReminderMinutes(int minutes) { this.reminders = new ArrayList<>(); if (minutes > 0) { @@ -653,7 +817,6 @@ public class CalendarEvent implements Serializable { } } - // Brukes hvis gammel kode prøver å hente ett tall. Returnerer det første i listen. public int getReminderMinutes() { if (reminders != null && !reminders.isEmpty()) { return reminders.get(0); @@ -676,6 +839,8 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarFullFragment.java package com.kbs.kbsintranett; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -721,16 +886,25 @@ public class CalendarFullFragment extends Fragment { layoutManager = new LinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager); backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); + } + @Override + public void onResume() { + super.onResume(); fetchAllEvents(); } private void fetchAllEvents() { progressBar.setVisibility(View.VISIBLE); - // Hent personlige hendelser - List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - // Hent felles hendelser fra WordPress Proxy (sikrer at vi får reminders) + new Thread(() -> { + // HER ER ENDRINGEN: isPreview = false (Hent alt) + List deviceEvents = CalendarManager.getDeviceEvents(getContext(), false); + new Handler(Looper.getMainLooper()).post(() -> fetchApiEvents(deviceEvents)); + }).start(); + } + + private void fetchApiEvents(List deviceEvents) { RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -740,7 +914,6 @@ public class CalendarFullFragment extends Fragment { List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { apiEvents = response.body(); - // Formater data for visning for (CalendarEvent e : apiEvents) { CalendarManager.formatEventForUI(e); } @@ -757,6 +930,7 @@ public class CalendarFullFragment extends Fragment { CalendarAdapter adapter = new CalendarAdapter(allEvents, event -> { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.setOnEventChangeListener(CalendarFullFragment.this::fetchAllEvents); sheet.show(getParentFragmentManager(), "CalendarDetails"); }); recyclerView.setAdapter(adapter); @@ -801,7 +975,6 @@ public class CalendarFullFragment extends Fragment { layoutManager.scrollToPositionWithOffset(scrollIndex, 0); } } - } ============================================================ @@ -818,8 +991,10 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.TimeZone; public class CalendarManager { @@ -853,7 +1028,13 @@ public class CalendarManager { return ids; } - public static List getDeviceEvents(Context context) { + /** + * Henter hendelser fra enheten. + * @param context App Context + * @param isPreview Hvis true: Henter kun kommende måned (Raskt). Hvis false: Henter -1 til +6 mnd (Tregere). + * @return Liste med events + */ + public static List getDeviceEvents(Context context, boolean isPreview) { List deviceEvents = new ArrayList<>(); if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { @@ -866,8 +1047,20 @@ public class CalendarManager { } long now = System.currentTimeMillis(); - long startMillis = now - (365L * 24 * 60 * 60 * 1000); - long endMillis = now + (365L * 24 * 60 * 60 * 1000); + long startMillis; + long endMillis; + + if (isPreview) { + // FORSIDEN: Optimalisert for hastighet. + // Henter fra NÅ og 30 dager frem i tid. + startMillis = now; + endMillis = now + (30L * 24 * 60 * 60 * 1000); + } else { + // FULL VISNING: Bredere tidsvindu. + // 1 mnd tilbake til 6 mnd frem. + startMillis = now - (30L * 24 * 60 * 60 * 1000); + endMillis = now + (180L * 24 * 60 * 60 * 1000); + } String[] projection = new String[]{ CalendarContract.Events.TITLE, @@ -927,9 +1120,11 @@ public class CalendarManager { } CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); - // Denne metoden eksisterer nå i CalendarEvent (se fil 1) event.setReminderMinutes(0); + event.setCalendarColor("#888888"); + event.setCalendarName("Min Kalender (Lokal)"); + formatEventForUI(event); deviceEvents.add(event); } @@ -1006,9 +1201,24 @@ public class CalendarManager { } public static List mergeAndSort(List apiEvents, List deviceEvents) { - List all = new ArrayList<>(apiEvents); - all.addAll(deviceEvents); + List all = new ArrayList<>(); + Set uniqueKeys = new HashSet<>(); + // 1. Legg til alle API-events først (Prioritert) + for (CalendarEvent apiEvent : apiEvents) { + all.add(apiEvent); + uniqueKeys.add(generateKey(apiEvent)); + } + + // 2. Legg til Device-events KUN hvis nøkkelen ikke finnes + for (CalendarEvent deviceEvent : deviceEvents) { + String key = generateKey(deviceEvent); + if (!uniqueKeys.contains(key)) { + all.add(deviceEvent); + } + } + + // 3. Sorter alt kronologisk Collections.sort(all, (e1, e2) -> { String d1 = e1.getRawDate() != null ? e1.getRawDate() : ""; String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; @@ -1018,6 +1228,22 @@ public class CalendarManager { return all; } + private static String generateKey(CalendarEvent event) { + String title = event.getTitle() != null ? event.getTitle().toLowerCase().trim() : ""; + String datePart = ""; + + if (event.getRawDate() != null) { + String digits = event.getRawDate().replaceAll("[^0-9]", ""); + if (digits.length() >= 12) { + datePart = digits.substring(0, 12); + } else if (digits.length() >= 8) { + datePart = digits.substring(0, 8); + } else { + datePart = digits; + } + } + return title + "_" + datePart; + } } ============================================================ @@ -1166,6 +1392,8 @@ package com.kbs.kbsintranett; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.TimePickerDialog; +import android.graphics.Color; +import android.graphics.Typeface; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -1297,12 +1525,21 @@ public class CreateEventFragment extends Fragment { private void prefillForm(CalendarEvent event) { etTitle.setText(event.getTitle()); - // Rens beskrivelsen for #varsel tag String cleanDesc = event.getDescription().replaceAll("#varsel:[\\d,]+", "").trim(); etDesc.setText(cleanDesc); etLocation.setText(event.getLocation()); - // Dato-parsing + // --- FIKS 404 FEIL VED OPPDATERING --- + ArrayAdapter adapter = (ArrayAdapter) spinnerCalendar.getAdapter(); + if (adapter != null) { + int position = adapter.getPosition(event.getCalendarName()); + if (position >= 0) { + spinnerCalendar.setSelection(position); + } + } + spinnerCalendar.setEnabled(false); + // ------------------------------------- + try { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @@ -1310,7 +1547,6 @@ public class CreateEventFragment extends Fragment { String start = event.getRawDate(); if (start != null) { if (start.length() == 10) { - // Heldags switchAllDay.setChecked(true); Date d = simpleFormat.parse(start); startCal.setTime(d); @@ -1325,7 +1561,6 @@ public class CreateEventFragment extends Fragment { endCal.setTime(d); } } else if (start.contains("T")) { - // Vanlig tid (kutt tidssone) if (start.length() > 19) start = start.substring(0, 19); startCal.setTime(isoFormat.parse(start)); @@ -1340,10 +1575,8 @@ public class CreateEventFragment extends Fragment { } } - // Varsler List existingReminders = event.getReminders(); if (!existingReminders.isEmpty()) { - // Fjern alle sjekkmerker først (15 min er default checked) for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { ((Chip) chipGroupReminders.getChildAt(i)).setChecked(false); } @@ -1366,14 +1599,67 @@ public class CreateEventFragment extends Fragment { } } - private void setupCalendarSpinner() { - // HENT DYNAMISK LISTE FRA USERMANAGER - List calendars = UserManager.getInstance().getWriteableCalendars(); + // --- NY LOGIKK FOR FARGER I SPINNER --- + private String getCalendarColor(String name) { + // Matcher fargene i PHP-config (V12.6) + switch (name) { + case "Felles": return "#0069B3"; // KBS Blå + case "Administrasjonen": return "#607D8B"; // Blue Grey + case "Serviceavdelingen": return "#E65100"; // Orange + case "Automasjonsavdelingen": return "#2E7D32"; // Green + case "Prosjektavdelingen": return "#7B1FA2"; // Purple + default: return "#888888"; // Grå fallback + } + } + + private void setupCalendarSpinner() { + List calendars = UserManager.getInstance().getWriteableCalendars(); if (calendars.isEmpty()) calendars.add("Felles"); - spinnerCalendar.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, calendars)); + // Vi bruker en Custom Adapter for å styre farger + ArrayAdapter adapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_item, calendars) { + + // getView: Dette er det som vises i selve boksen når noe er valgt + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + TextView view = (TextView) super.getView(position, convertView, parent); + + String calName = getItem(position); + String colorHex = getCalendarColor(calName); + + // Sett bakgrunnsfarge lik kalenderfarge + view.setBackgroundColor(Color.parseColor(colorHex)); + + // Hvit tekst for kontrast + view.setTextColor(Color.WHITE); + view.setTypeface(null, Typeface.BOLD); + + return view; + } + + // getDropDownView: Dette er listen som popper opp + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + TextView view = (TextView) super.getDropDownView(position, convertView, parent); + + String calName = getItem(position); + String colorHex = getCalendarColor(calName); + + // Her holder vi bakgrunnen hvit, men farger teksten + view.setBackgroundColor(Color.WHITE); + view.setTextColor(Color.parseColor(colorHex)); + view.setTypeface(null, Typeface.BOLD); + + return view; + } + }; + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCalendar.setAdapter(adapter); } + // -------------------------------------- private void setupReminderChips() { addChip("Ved start", 0); @@ -1624,7 +1910,6 @@ public class CreateEventFragment extends Fragment { } } - // NY: Henter navnet på kalenderen direkte fra Spinneren private String getCalendarSlug() { if (spinnerCalendar.getSelectedItem() != null) { return spinnerCalendar.getSelectedItem().toString(); @@ -1796,14 +2081,15 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonArray; -import com.google.gson.JsonParser; import org.json.JSONArray; import org.json.JSONException; @@ -1855,11 +2141,12 @@ public class FormsFragment extends Fragment { private LinearLayout formContainer; private LinearLayout historyContainer; - private View historyWrapper; // Wrapper for historikk-modulen + private View historyWrapper; private TextView txtStatus; private TextView lblHistory; private ProgressBar loadingSpinner; private ImageView btnToggleHistory; + private Toolbar toolbar; // NYTT // --- HOVEDSKJEMA STATE --- private Map fieldWrappers = new HashMap<>(); @@ -1871,7 +2158,7 @@ public class FormsFragment extends Fragment { private Map childInputViews = new HashMap<>(); private Map childRequiredFieldsMap = new HashMap<>(); private Map childFileUploads = new HashMap<>(); - // Lagring av Nested Entries + private List nestedEntries = new ArrayList<>(); private LinearLayout nestedEntriesContainer; private TextView totalAmountView; @@ -1937,12 +2224,17 @@ public class FormsFragment extends Fragment { lblHistory = view.findViewById(R.id.lbl_history); loadingSpinner = view.findViewById(R.id.loading_spinner); + // NYTT: Finn toolbar og sett listener + toolbar = view.findViewById(R.id.forms_toolbar); + if (toolbar != null) { + toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).navigateUp()); + } + btnToggleHistory = view.findViewById(R.id.btn_toggle_history); if (btnToggleHistory != null) { btnToggleHistory.setOnClickListener(v -> toggleHistoryVisibility()); } - // --- FIKS FOR NULLPOINTER EXCEPTION PÅ LAYOUTTRANSITION --- if (view instanceof ViewGroup) { LayoutTransition transition = ((ViewGroup) view).getLayoutTransition(); if (transition == null) { @@ -1951,7 +2243,6 @@ public class FormsFragment extends Fragment { } transition.enableTransitionType(LayoutTransition.CHANGING); } - // ---------------------------------------------------------- if (formContainer == null) { formContainer = new LinearLayout(getContext()); @@ -1982,11 +2273,9 @@ public class FormsFragment extends Fragment { if (historyWrapper == null || btnToggleHistory == null) return; if (historyWrapper.getVisibility() == View.VISIBLE) { - // Skjul historikk historyWrapper.setVisibility(View.GONE); btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float); } else { - // Vis historikk historyWrapper.setVisibility(View.VISIBLE); btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float); } @@ -2053,20 +2342,17 @@ public class FormsFragment extends Fragment { nestedEntries.clear(); updateStatus(""); - // Reset visibility of history on new load + // NYTT: Sett tittelen i Toolbaren i stedet for å legge til en TextView + if (toolbar != null) { + toolbar.setTitle(getCleanTitle(form.title)); + } + if (historyWrapper != null) { historyWrapper.setVisibility(View.VISIBLE); if (btnToggleHistory != null) btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float); } - TextView title = new TextView(getContext()); - title.setText(getCleanTitle(form.title)); - title.setTextSize(24); - title.setTypeface(null, Typeface.BOLD); - title.setTextColor(Color.BLACK); - title.setPadding(0, 0, 0, 20); - formContainer.addView(title); - + // Beskrivelse legges fortsatt inn som innhold if (form.description != null && !form.description.isEmpty()) { TextView formDesc = new TextView(getContext()); String cleanDesc = form.description.replaceFirst("^\\d+\\.\\s*", ""); @@ -2182,14 +2468,13 @@ public class FormsFragment extends Fragment { btnAdd.setBackgroundColor(Color.parseColor("#53AFE9")); btnAdd.setTextColor(Color.WHITE); btnAdd.setOnClickListener(v -> { - expandFormModule(); // Trigger expand + expandFormModule(); int childFormId = 18; if (field.gpnfForm != null) { try { childFormId = Integer.parseInt(field.gpnfForm); } catch (NumberFormatException e) { e.printStackTrace(); } } - // NYTT: Sender med felt-ID fra hovedskjemaet (f.eks "25") openChildFormDialog(childFormId, field.id); }); container.addView(btnAdd); @@ -2208,7 +2493,6 @@ public class FormsFragment extends Fragment { container.addView(totalAmountView); } - // NY SIGNATUR: Tar imot parentFieldId private void openChildFormDialog(int childFormId, String parentFieldId) { if (getActivity() == null) return; ProgressBar pBar = new ProgressBar(getContext()); @@ -2236,7 +2520,6 @@ public class FormsFragment extends Fragment { }); } - // NY SIGNATUR: Tar imot parentFieldId private void showChildFormDialog(GravityForm childForm, String parentFieldId) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); childInputViews.clear(); @@ -2286,7 +2569,6 @@ public class FormsFragment extends Fragment { }); } - // NY SIGNATUR: Tar imot parentFieldId og sender den som meta private void submitChildForm(int childFormId, AlertDialog dialog, String parentFieldId) { JSONObject inputValues = new JSONObject(); for (Map.Entry entry : childInputViews.entrySet()) { @@ -2312,13 +2594,9 @@ public class FormsFragment extends Fragment { } } - // --- HER ER FIKSEN FOR BUGGEN --- - // Vi legger ved en ekstra parameter som forteller Gravity Forms at dette - // er en nested entry som hører til et bestemt felt (f.eks "25"). if (parentFieldId != null) { textParts.put("gpnf_entry_nested_form_field", RequestBody.create(MultipartBody.FORM, parentFieldId)); } - // --------------------------------- for (Map.Entry fileEntry : childFileUploads.entrySet()) { String fieldId = fileEntry.getKey(); @@ -2338,9 +2616,6 @@ public class FormsFragment extends Fragment { JsonObject json = response.body().getAsJsonObject(); if (json.has("is_valid") && json.get("is_valid").getAsBoolean()) { String entryId = json.has("entry_id") ? json.get("entry_id").getAsString() : ""; - - // NB: Tilpass ID-ene her hvis skjema 18 endres. - // ID 3 = Beskrivelse, ID 4 = Beløp String desc = getInputValueGeneric(childInputViews.get("3")); String price = getInputValueGeneric(childInputViews.get("4")); addNestedEntry(entryId, desc, price); @@ -2414,10 +2689,9 @@ public class FormsFragment extends Fragment { Button btnUpload = new Button(getContext()); btnUpload.setText("Velg fil / Ta bilde"); btnUpload.setOnClickListener(v -> { - // Setter state før vi viser dialog pendingFileFieldId = field.id; isSelectingForChild = isChild; - expandFormModule(); // Trigger expand + expandFormModule(); showFileSourceDialog(); }); TextView txtFileName = new TextView(getContext()); @@ -2434,23 +2708,19 @@ public class FormsFragment extends Fragment { reqMap.put(field.id, field.isRequired); } - // Hjelpemetode for å vise dialog private void showFileSourceDialog() { String[] options = {"Ta bilde", "Velg fil"}; new AlertDialog.Builder(getContext()) .setTitle("Last opp vedlegg") .setItems(options, (dialog, which) -> { if (which == 0) { - // Ta bilde - SJEKKER PERMISSION FØRST if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { openCamera(); } else { - // Spør om lov requestPermissionLauncher.launch(Manifest.permission.CAMERA); } } else { - // Velg fil if (filePickerLauncher != null) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); @@ -2543,7 +2813,7 @@ public class FormsFragment extends Fragment { timeInput.setClickable(true); timeInput.setHint("00:00"); timeInput.setOnClickListener(v -> { - expandFormModule(); // Trigger expand + expandFormModule(); Calendar mcurrentTime = Calendar.getInstance(); int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY); int minute = mcurrentTime.get(Calendar.MINUTE); @@ -2584,7 +2854,7 @@ public class FormsFragment extends Fragment { public void onTextChanged(CharSequence s, int start, int before, int count) {} public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); } }); - attachInteractionListener(input); // Add listener + attachInteractionListener(input); container.addView(input); views.put(field.id, input); @@ -2598,7 +2868,7 @@ public class FormsFragment extends Fragment { input.setMinLines(3); input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START); - attachInteractionListener(input); // Add listener + attachInteractionListener(input); container.addView(input); views.put(field.id, input); @@ -2612,7 +2882,6 @@ public class FormsFragment extends Fragment { RadioButton rb = new RadioButton(getContext()); rb.setText(choice.text); rb.setTag(choice.value); - // Also trigger expand on RadioButton click rb.setOnClickListener(v -> { expandFormModule(); evaluateAllConditionalLogic(); @@ -2620,7 +2889,6 @@ public class FormsFragment extends Fragment { group.addView(rb); } } - // Fallback listener group.setOnCheckedChangeListener((g, i) -> { expandFormModule(); evaluateAllConditionalLogic(); @@ -2632,7 +2900,6 @@ public class FormsFragment extends Fragment { private void renderSelectField(LinearLayout container, GravityField field, Map views, Map req) { Spinner spinner = new Spinner(getContext()); - // Spinner touch listener is tricky, usually set onTouchListener works spinner.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { expandFormModule(); @@ -2675,7 +2942,6 @@ public class FormsFragment extends Fragment { checkBox.setText(inputDef.label); String value = "1"; - // Fallback if (field.choices != null && i < field.choices.size()) { value = field.choices.get(i).value; } @@ -2693,24 +2959,20 @@ public class FormsFragment extends Fragment { private void renderDateField(LinearLayout container, GravityField field, Map views, Map req) { EditText dateInput = new EditText(getContext()); - // --- DATO LØSNING (Fix for Read Only / Dagens Dato) --- if (field.readOnly || (formId == ID_REFUSJON_UTLEGG && "28".equals(field.id))) { - // Sett dagens dato SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); dateInput.setText(df.format(new Date())); - // Gjør den "read-only" men synlig dateInput.setFocusable(false); dateInput.setClickable(false); dateInput.setEnabled(false); dateInput.setTextColor(Color.BLACK); } else { - // Vanlig dato-velger dateInput.setFocusable(false); dateInput.setClickable(true); dateInput.setHint("dd.mm.yyyy"); dateInput.setOnClickListener(v -> { - expandFormModule(); // Trigger expand + expandFormModule(); Calendar c = Calendar.getInstance(); new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> { dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year)); @@ -3114,20 +3376,17 @@ public class FormsFragment extends Fragment { } try { - // Vis flere oppføringer siden vi nå har scrolle-mulighet øverst int count = Math.min(entries.length(), 20); for (int i = 0; i < count; i++) { JSONObject entry = entries.getJSONObject(i); String date = entry.optString("date_created"); - // Prøv å finne en bedre tittel enn bare dato (f.eks Prosjektnavn eller Sted) String titleText = "Innsendt: " + date; TextView item = new TextView(getContext()); item.setText(titleText); item.setPadding(10, 20, 10, 20); item.setBackgroundResource(android.R.drawable.list_selector_background); item.setTextSize(14); - // Add click listener to show details item.setOnClickListener(v -> showEntryDetails(entry)); View line = new View(getContext()); line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); @@ -3140,20 +3399,16 @@ public class FormsFragment extends Fragment { } } - // NY METODE: Vis detaljer i dialog med delingsknapp private void showEntryDetails(JSONObject entry) { - // HVIS dette er Form 16 (Refusjon), må vi først hente vedleggene (som ligger i Form 18) if (formId == ID_REFUSJON_UTLEGG) { Log.d(TAG, "Form 16 detected. Checking for child entries..."); - String nestedIds = entry.optString("25"); // Felt 25 i Form 16 inneholder ID-ene til vedleggene + String nestedIds = entry.optString("25"); if (!nestedIds.isEmpty()) { Log.d(TAG, "Nested IDs found: " + nestedIds); List ids = new ArrayList<>(); - // ROBUST PARSING AV ID-LISTE if (nestedIds.startsWith("[") && nestedIds.endsWith("]")) { - // Dette er en JSON-array streng (f.eks [101, 102]) try { JSONArray jsonArray = new JSONArray(nestedIds); for(int i=0; i").append(field.label).append(":
") @@ -3244,27 +3488,20 @@ public class FormsFragment extends Fragment { } catch (Exception e) {} } - // Rekursiv metode for å hente barn-oppføringer (Vedlegg) private void fetchChildEntriesRecursive(List ids, int index, StringBuilder html, StringBuilder text, AlertDialog loader) { if (index >= ids.size()) { - // Alle ferdige! Vis dialogen. loader.dismiss(); showFinalDialog(html, text); return; } String entryId = ids.get(index); - Log.d(TAG, "Fetching child entry ID: " + entryId); - RetrofitClient.getApiService().getSingleEntry(entryId).enqueue(new retrofit2.Callback() { @Override public void onResponse(retrofit2.Call call, retrofit2.Response response) { if (response.isSuccessful() && response.body() != null) { try { JsonObject json = response.body().getAsJsonObject(); - // I Form 18: Felt 1 = Fil, Felt 3 = Beskrivelse, Felt 4 = Beløp - - // Parse Description and Price String desc = json.has("3") ? json.get("3").getAsString() : "Uten beskrivelse"; String price = json.has("4") ? json.get("4").getAsString() : ""; @@ -3274,11 +3511,9 @@ public class FormsFragment extends Fragment { html.append(desc).append(" (").append(price).append(")
"); text.append(desc).append(" (").append(price).append(")\n"); - // Parse File Field (ID 1) - Can be multiple files! if (json.has("1")) { JsonElement fileEl = json.get("1"); if (fileEl.isJsonArray()) { - // It is a real JSON array JsonArray arr = fileEl.getAsJsonArray(); for (int i = 0; i < arr.size(); i++) { String url = arr.get(i).getAsString().replace("\\/", "/"); @@ -3286,7 +3521,6 @@ public class FormsFragment extends Fragment { text.append(url).append("\n"); } } else { - // It is a string. Check if it's a JSON string array like "[\"http...\"]" String rawString = fileEl.getAsString(); if (rawString.startsWith("[") && rawString.endsWith("]")) { try { @@ -3297,13 +3531,11 @@ public class FormsFragment extends Fragment { text.append(url).append("\n"); } } catch (JSONException ex) { - // Fallback: simple cleanup String clean = extractUrl(rawString); html.append("Åpne fil
"); text.append(clean).append("\n"); } } else { - // Just a plain URL string String clean = extractUrl(rawString); if(clean.startsWith("http")) { html.append("Åpne fil
"); @@ -3318,17 +3550,12 @@ public class FormsFragment extends Fragment { } catch (Exception e) { Log.e(TAG, "Error parsing child entry", e); } - } else { - Log.e(TAG, "Failed to fetch child entry: " + response.code()); } - // Gå til neste (uansett om denne feilet eller ei) fetchChildEntriesRecursive(ids, index + 1, html, text, loader); } @Override public void onFailure(retrofit2.Call call, Throwable t) { - Log.e(TAG, "Network error fetching child entry", t); - // Hopp over ved feil fetchChildEntriesRecursive(ids, index + 1, html, text, loader); } }); @@ -3351,7 +3578,6 @@ public class FormsFragment extends Fragment { .show(); } - // Hjelpemetode for å rydde opp i URLer fra JSON (f.eks ["http://..."] -> http://...) private String extractUrl(String rawValue) { if (rawValue == null) return ""; String clean = rawValue.replace("[", "") @@ -4517,11 +4743,15 @@ package com.kbs.kbsintranett; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -4531,6 +4761,7 @@ import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import java.text.SimpleDateFormat; @@ -4547,6 +4778,11 @@ import retrofit2.Response; public class HomeFragment extends Fragment { private ActivityResultLauncher requestPermissionLauncher; private RecyclerView calendarRecycler; + private RecyclerView newsRecycler; + private ProgressBar mainProgressBar; + private SwipeRefreshLayout swipeRefreshLayout; + + private int activeNetworkCalls = 0; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -4555,7 +4791,7 @@ public class HomeFragment extends Fragment { new ActivityResultContracts.RequestPermission(), isGranted -> { if (calendarRecycler != null) { - fetchCalendarEvents(calendarRecycler); + refreshData(); } } ); @@ -4572,15 +4808,19 @@ public class HomeFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + mainProgressBar = view.findViewById(R.id.main_loading_spinner); + if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE); + + swipeRefreshLayout = view.findViewById(R.id.swipe_refresh_home); + swipeRefreshLayout.setOnRefreshListener(this::refreshData); + View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); } - // NY LOGIKK: Vis knapp hvis brukeren har tilgang til minst én kalender Button btnCreateEvent = view.findViewById(R.id.btn_create_event); List writeable = UserManager.getInstance().getWriteableCalendars(); - if (writeable != null && !writeable.isEmpty()) { btnCreateEvent.setVisibility(View.VISIBLE); btnCreateEvent.setOnClickListener(v -> { @@ -4599,19 +4839,13 @@ public class HomeFragment extends Fragment { viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull)); } - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { - fetchCalendarEvents(calendarRecycler); - } else { - requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); - } - if (android.os.Build.VERSION.SDK_INT >= 33) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS); } } - RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); + newsRecycler = view.findViewById(R.id.recycler_news); newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); newsRecycler.setNestedScrollingEnabled(false); newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); @@ -4623,12 +4857,47 @@ public class HomeFragment extends Fragment { }); } + refreshData(); + } + + @Override + public void onResume() { + super.onResume(); + if (activeNetworkCalls == 0) { + refreshData(); + } + } + + private void refreshData() { + activeNetworkCalls = 2; + + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { + fetchCalendarEvents(calendarRecycler); + } else { + checkLoadingComplete(); + requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); + } + fetchNewsFromWordpress(newsRecycler); } - private void fetchCalendarEvents(RecyclerView recyclerView) { - List deviceEvents = CalendarManager.getDeviceEvents(getContext()); + private void checkLoadingComplete() { + activeNetworkCalls--; + if (activeNetworkCalls <= 0) { + activeNetworkCalls = 0; + if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + } + } + private void fetchCalendarEvents(RecyclerView recyclerView) { + new Thread(() -> { + List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); + new Handler(Looper.getMainLooper()).post(() -> fetchApiEvents(recyclerView, deviceEvents)); + }).start(); + } + + private void fetchApiEvents(RecyclerView recyclerView, List deviceEvents) { RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -4637,51 +4906,64 @@ public class HomeFragment extends Fragment { List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { apiEvents = response.body(); + + CacheManager.saveCalendarEvents(getContext(), apiEvents); + for (CalendarEvent e : apiEvents) { CalendarManager.formatEventForUI(e); } - } - - List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - List upcomingEvents = new ArrayList<>(); - String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - - for (CalendarEvent e : merged) { - if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { - upcomingEvents.add(e); + } else { + apiEvents = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); + if (!apiEvents.isEmpty()) { + Toast.makeText(getContext(), "Server utilgjengelig. Viser lagret kalender.", Toast.LENGTH_SHORT).show(); } } - List top5 = new ArrayList<>(); - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); + updateCalendarUI(recyclerView, apiEvents, deviceEvents); + checkLoadingComplete(); } @Override public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; - if (!deviceEvents.isEmpty()) { - List top5 = new ArrayList<>(); - for(int i=0; i { - CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); - sheet.show(getParentFragmentManager(), "CalendarDetails"); - })); - } else { - List errorList = new ArrayList<>(); - errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS", "")); - recyclerView.setAdapter(new CalendarAdapter(errorList, null)); + + List cachedApiEvents = CacheManager.getCachedCalendarEvents(getContext()); + for (CalendarEvent e : cachedApiEvents) CalendarManager.formatEventForUI(e); + + if (!cachedApiEvents.isEmpty()) { + Toast.makeText(getContext(), "Ingen nettverk. Viser lagret kalender.", Toast.LENGTH_SHORT).show(); } + + updateCalendarUI(recyclerView, cachedApiEvents, deviceEvents); + checkLoadingComplete(); } }); } + private void updateCalendarUI(RecyclerView recyclerView, List apiEvents, List deviceEvents) { + List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); + List upcomingEvents = new ArrayList<>(); + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + + for (CalendarEvent e : merged) { + if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { + upcomingEvents.add(e); + } + } + + List topEvents = new ArrayList<>(); + for(int i=0; i { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.setOnEventChangeListener(HomeFragment.this::refreshData); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + })); + } + private void fetchNewsFromWordpress(RecyclerView recyclerView) { WordPressApiService apiService = RetrofitClient.getApiService(); apiService.getPosts().enqueue(new Callback>() { @@ -4689,37 +4971,58 @@ public class HomeFragment extends Fragment { public void onResponse(Call> call, Response> response) { if (getContext() == null) return; + List postsToShow = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null) { - List wpPosts = response.body(); - SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); - targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - - for (WpPost post : wpPosts) { - try { - Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); - } catch (Exception e) {} + postsToShow = response.body(); + CacheManager.saveNewsPosts(getContext(), postsToShow); + } else { + postsToShow = CacheManager.getCachedNewsPosts(getContext()); + if (!postsToShow.isEmpty()) { + Toast.makeText(getContext(), "Server utilgjengelig. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show(); } - - NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { - Bundle bundle = new Bundle(); - bundle.putSerializable("post_data", post); - Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); - }); - recyclerView.setAdapter(adapter); } + + updateNewsUI(recyclerView, postsToShow); + checkLoadingComplete(); } @Override public void onFailure(Call> call, Throwable t) { if (getContext() == null) return; - recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); + + List cachedPosts = CacheManager.getCachedNewsPosts(getContext()); + if (!cachedPosts.isEmpty()) { + Toast.makeText(getContext(), "Ingen nettverk. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show(); + } + + updateNewsUI(recyclerView, cachedPosts); + checkLoadingComplete(); } }); } + private void updateNewsUI(RecyclerView recyclerView, List posts) { + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + for (WpPost post : posts) { + try { + Date date = rawFormat.parse(post.date); + post.date = targetFormat.format(date); + } catch (Exception e) {} + } + + NewsAdapter adapter = new NewsAdapter(posts, post -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); + Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); + }); + recyclerView.setAdapter(adapter); + } + private void startNotificationWorker() { PeriodicWorkRequest notifRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) @@ -4728,6 +5031,100 @@ public class HomeFragment extends Fragment { } } +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\ImageDialogFragment.java +============================================================ +package com.kbs.kbsintranett; + +import android.app.Dialog; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; + +public class ImageDialogFragment extends DialogFragment { + + private static final String ARG_URL = "image_url"; + + public static ImageDialogFragment newInstance(String imageUrl) { + ImageDialogFragment fragment = new ImageDialogFragment(); + Bundle args = new Bundle(); + args.putString(ARG_URL, imageUrl); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onStart() { + super.onStart(); + // Gjør dialogen fullskjerm + Dialog dialog = getDialog(); + if (dialog != null) { + int width = ViewGroup.LayoutParams.MATCH_PARENT; + int height = ViewGroup.LayoutParams.MATCH_PARENT; + dialog.getWindow().setLayout(width, height); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_image_dialog, container, false); + + ImageView imageView = view.findViewById(R.id.full_screen_image); + ImageButton closeBtn = view.findViewById(R.id.btn_close_image); + ProgressBar progressBar = view.findViewById(R.id.loading_image); + + String url = getArguments() != null ? getArguments().getString(ARG_URL) : null; + + if (url != null) { + Glide.with(this) + .load(url) + .transition(DrawableTransitionOptions.withCrossFade()) + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { + progressBar.setVisibility(View.GONE); + return false; + } + + @Override + public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { + progressBar.setVisibility(View.GONE); + return false; + } + }) + .into(imageView); + } + + closeBtn.setOnClickListener(v -> dismiss()); + // Lukk også hvis man trykker på selve bildet + imageView.setOnClickListener(v -> dismiss()); + + return view; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\InputsAdapter.java ============================================================ @@ -5155,14 +5552,12 @@ import java.util.concurrent.TimeUnit; public class MainActivity extends AppCompatActivity { - // VIKTIG: Sørg for at denne matcher den du har i Google Cloud Console - public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; + public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com"; // Bytt med din egen hvis denne er feil private static final String TAG = "MainActivity"; private NavController navController; private BottomNavigationView bottomNav; - // Launcher for å spørre om varslingstillatelse (Android 13+) private ActivityResultLauncher requestPermissionLauncher; @Override @@ -5171,7 +5566,6 @@ public class MainActivity extends AppCompatActivity { setContentView(R.layout.activity_main); // --- 1. SETUP UI & NAVIGASJON --- - // Sjekket activity_main.xml: ID er "bottom_nav_view" bottomNav = findViewById(R.id.bottom_nav_view); NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() @@ -5181,6 +5575,13 @@ public class MainActivity extends AppCompatActivity { navController = navHostFragment.getNavController(); if (bottomNav != null) { NavigationUI.setupWithNavController(bottomNav, navController); + + // --- NYTT: Håndter "Reselection" (Klikk på fanen man allerede er i) --- + bottomNav.setOnItemReselectedListener(item -> { + // Dette fjerner alt som ligger "oppå" hovedsiden i stabelen. + // F.eks: Hjem -> Kalender Full -> (Klikk Hjem) -> Hjem + navController.popBackStack(item.getItemId(), false); + }); } // Skjul meny på login-skjerm @@ -5195,32 +5596,26 @@ public class MainActivity extends AppCompatActivity { }); } - // --- 2. VARSLINGSOPPSETT (NYTT) --- + // --- 2. VARSLINGSOPPSETT --- createNotificationChannel(); - // Initialiser permission launcher for varsler requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { Log.d(TAG, "Varslingstillatelse gitt!"); } else { - Log.e(TAG, "Varslingstillatelse avslått. Bruker får ikke kalendervarsler."); + Log.e(TAG, "Varslingstillatelse avslått."); } }); - // Sjekk tillatelser (både Varsler og Alarmer) checkNotificationPermission(); checkExactAlarmPermission(); - // Start bakgrunnsjobben for kalenderen scheduleCalendarWork(); - // --- 3. AUTENTISERING (GAMMELT) --- + // --- 3. AUTENTISERING --- checkLoginState(); } - /** - * Sjekker om brukeren er logget inn med Google, og gjør en silent refresh mot WP. - */ private void checkLoginState() { GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); if (account == null) { @@ -5279,8 +5674,6 @@ public class MainActivity extends AppCompatActivity { } } - // --- NYE HJELPEMETODER FOR VARSLING --- - private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = "KBS Kalendervarsler"; @@ -5303,10 +5696,6 @@ public class MainActivity extends AppCompatActivity { } } - /** - * Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM). - * Hvis ikke, spør brukeren om å gå til innstillinger. - */ private void checkExactAlarmPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); @@ -5325,9 +5714,6 @@ public class MainActivity extends AppCompatActivity { } } - /** - * Starter WorkManager som sjekker kalenderen hvert 15. minutt. - */ private void scheduleCalendarWork() { PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) .build(); @@ -5452,12 +5838,15 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsDetailFragment.java ============================================================ package com.kbs.kbsintranett; +import android.content.Context; import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; @@ -5469,6 +5858,41 @@ import com.bumptech.glide.Glide; public class NewsDetailFragment extends Fragment { + // CSS Styling (Samme stil som håndboken, pluss bildehåndtering) + private static final String CSS_STYLE = + ""; + + // JavaScript for å fange opp bildeklikk + private static final String JS_SCRIPT = + ""; + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -5479,7 +5903,6 @@ public class NewsDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Hent data fra argumentene (sendt fra HomeFragment/NewsFullFragment) if (getArguments() != null) { WpPost post = (WpPost) getArguments().getSerializable("post_data"); if (post != null) { @@ -5496,9 +5919,10 @@ public class NewsDetailFragment extends Fragment { TextView title = view.findViewById(R.id.detail_title); TextView category = view.findViewById(R.id.detail_category); TextView date = view.findViewById(R.id.detail_date); - TextView author = view.findViewById(R.id.detail_author); // NY - TextView content = view.findViewById(R.id.detail_content); + TextView author = view.findViewById(R.id.detail_author); + WebView webView = view.findViewById(R.id.detail_webview); + // Header bilde String imgUrl = post.getFeaturedImageUrl(); if (imgUrl != null) { Glide.with(this).load(imgUrl).centerCrop().into(image); @@ -5509,12 +5933,60 @@ public class NewsDetailFragment extends Fragment { title.setText(post.getTitleStr()); category.setText(post.getCategoryName()); date.setText("Publisert: " + post.date); - - // NYTT: Sett forfatter author.setText("Av: " + post.getAuthorName()); - content.setText(Html.fromHtml(post.getContentStr(), Html.FROM_HTML_MODE_COMPACT)); - content.setMovementMethod(LinkMovementMethod.getInstance()); + // Konfigurer WebView + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + + // Legg til Interface for å snakke med Java + webView.addJavascriptInterface(new WebAppInterface(getContext()), "Android"); + + // Håndter linker internt (som i Håndboken) + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // Bruk samme link-logikk som i Håndboken hvis nødvendig, + // men her lar vi linker åpnes i nettleser for enkelhets skyld foreløpig + return false; + } + }); + + // Bygg HTML + String rawContent = post.getContentStr(); + + // Vask innholdet litt hvis nødvendig (f.eks fjerne inline styles som ødelegger) + // Her legger vi bare til vår CSS og JS + String htmlData = "" + + "" + + CSS_STYLE + + JS_SCRIPT + + "" + + rawContent + + ""; + + webView.loadDataWithBaseURL("https://intranet.kbs.no", htmlData, "text/html", "UTF-8", null); + } + + // Bridge-klasse for å ta imot klikk fra JavaScript + public class WebAppInterface { + Context mContext; + + WebAppInterface(Context c) { + mContext = c; + } + + @JavascriptInterface + public void showImage(String url) { + // Må kjøres på UI-tråden + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + ImageDialogFragment dialog = ImageDialogFragment.newInstance(url); + dialog.show(getParentFragmentManager(), "image_lightbox"); + }); + } + } } } @@ -5841,22 +6313,24 @@ public class ProfileFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_profile, container, false); - // 1. Finn Views ImageView closeBtn = view.findViewById(R.id.btn_close_profile); ImageView profileImage = view.findViewById(R.id.profile_image); TextView nameText = view.findViewById(R.id.profile_name); TextView emailText = view.findViewById(R.id.profile_email); TextView roleText = view.findViewById(R.id.profile_role); Button logoutBtn = view.findViewById(R.id.btn_logout); - Button updateInfoBtn = view.findViewById(R.id.btn_update_info); // NY + Button updateInfoBtn = view.findViewById(R.id.btn_update_info); + TextView versionText = view.findViewById(R.id.tv_version_info); // NYTT - // 2. Hent data fra UserManager UserManager user = UserManager.getInstance(); nameText.setText(user.getUserDisplayName()); emailText.setText(user.getUserEmail()); roleText.setText("Rolle: " + user.getUserRole()); - // 3. Last bilde med Glide + // NYTT: Sett versjonstekst + String versionInfo = "Versjon " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")"; + versionText.setText(versionInfo); + if (user.getPhotoUrl() != null) { Glide.with(this) .load(user.getPhotoUrl()) @@ -5864,44 +6338,33 @@ public class ProfileFragment extends Fragment { .into(profileImage); } - // 4. Håndter "Lukk" (X) knapp - Gå tilbake til forrige skjerm closeBtn.setOnClickListener(v -> { Navigation.findNavController(view).navigateUp(); }); - // 5. Håndter "Oppdater opplysninger" (Skjema ID 1) updateInfoBtn.setOnClickListener(v -> { Bundle bundle = new Bundle(); - bundle.putInt("formId", 1); // ID 1 er Ansatteopplysninger + bundle.putInt("formId", 1); Navigation.findNavController(view).navigate(R.id.action_profile_to_form, bundle); }); - // 6. Håndter utlogging logoutBtn.setOnClickListener(v -> performLogout()); return view; } private void performLogout() { - // A. Konfigurer Google Client GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(MainActivity.GOOGLE_WEB_CLIENT_ID) .requestEmail() .build(); GoogleSignInClient client = GoogleSignIn.getClient(requireActivity(), gso); - // B. Logg ut fra Google client.signOut().addOnCompleteListener(task -> { - - // C. Tøm interne data UserManager.getInstance().logout(); RetrofitClient.clearClient(); - - // D. Naviger tilbake til Login-skjermen NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); - // Denne aksjonen finnes i mobile_navigation.xml navController.navigate(R.id.action_profile_to_login); - Toast.makeText(getContext(), "Du er nå logget ut", Toast.LENGTH_SHORT).show(); }); } @@ -6363,6 +6826,15 @@ FILSTI: app\src\main\res\drawable\bg_category_unselected.xml +============================================================ +FILSTI: app\src\main\res\drawable\bg_date_box.xml +============================================================ + + + + + + ============================================================ FILSTI: app\src\main\res\drawable\ic_book.xml ============================================================ @@ -6667,6 +7139,21 @@ FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml android:orientation="vertical" android:padding="24dp" android:background="@android:color/white"> + + + + -