Dette er kildekoden til et Android Studio-prosjekt. Hver fil er separert med overskrifter. ============================================================ 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 ============================================================ pluginManagement { repositories { google { content { includeGroupByRegex("com\\.android.*") includeGroupByRegex("com\\.google.*") includeGroupByRegex("androidx.*") } } mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "KBS Intranett" include(":app") ============================================================ FILSTI: app\build.gradle.kts ============================================================ import java.util.Properties import java.io.FileInputStream plugins { alias(libs.plugins.android.application) // NY LINJE: Aktiver Google Services plugin her id("com.google.gms.google-services") } // --- NY KODE: Last inn local.properties --- // Vi bruker "Properties()" direkte siden vi har importert den på toppen val localProperties = Properties() val localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { localProperties.load(FileInputStream(localPropertiesFile)) } // ------------------------------------------ android { namespace = "com.kbs.kbsintranett" compileSdk = 34 defaultConfig { applicationId = "com.kbs.kbsintranett" minSdk = 28 targetSdk = 34 versionCode = 4 versionName = "1.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Hent verdien vi lastet inn på toppen av filen val webClientId = localProperties.getProperty("WEB_CLIENT_ID") ?: "" // Opprett BuildConfig-feltet. // Vi legger på ekstra hermetegn (\") for at det skal bli en String i Java-koden. buildConfigField("String", "WEB_CLIENT_ID", "\"$webClientId\"") } // NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden buildFeatures { buildConfig = true } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } dependencies { implementation(libs.appcompat) implementation(libs.material) implementation(libs.activity) implementation(libs.constraintlayout) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) // Nettverk og JSON-håndtering implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.google.code.gson:gson:2.10.1") // Navigation Component val navVersion = "2.8.5" implementation("androidx.navigation:navigation-fragment:$navVersion") implementation("androidx.navigation:navigation-ui:$navVersion") implementation("com.google.android.gms:play-services-auth:20.7.0") implementation("com.github.bumptech.glide:glide:4.16.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") implementation("androidx.work:work-runtime:2.9.0") // 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") // NYTT: Firebase Cloud Messaging lagt til her implementation("com.google.firebase:firebase-messaging") } ============================================================ FILSTI: app\proguard-rules.pro ============================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ============================================================ FILSTI: app\src\androidTest\java\com\kbs\kbsintranett\ExampleInstrumentedTest.java ============================================================ package com.kbs.kbsintranett; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.kbs.kbsintranett", appContext.getPackageName()); } } ============================================================ FILSTI: app\src\main\AndroidManifest.xml ============================================================ ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\AddTaskBottomSheet.java ============================================================ package com.kbs.kbsintranett; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.Dialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.Map; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class AddTaskBottomSheet extends BottomSheetDialogFragment { private EditText etTitle, etDesc; private Button btnDate, btnUsers, btnSave, btnClearDate; // NYTT private TextView txtSheetTitle, txtDatePreview, txtUsersPreview; private Calendar dueDate = Calendar.getInstance(); private boolean hasDate = true; // NYTT private List filteredUsers = new ArrayList<>(); private List selectedUsers = new ArrayList<>(); private TaskItem taskToEdit = null; public interface OnTaskAddedListener { void onTaskAdded(TaskItem task); void onTaskUpdated(TaskItem task); } private OnTaskAddedListener listener; public void setOnTaskAddedListener(OnTaskAddedListener listener) { this.listener = listener; } public void setTaskToEdit(TaskItem task) { this.taskToEdit = task; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); dialog.setOnShowListener(dialogInterface -> { BottomSheetDialog d = (BottomSheetDialog) dialogInterface; View bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet); if (bottomSheet != null) { BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED); } }); return dialog; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View v = inflater.inflate(R.layout.bottom_sheet_add_task, container, false); txtSheetTitle = v.findViewById(R.id.txt_sheet_title); etTitle = v.findViewById(R.id.et_task_title); etDesc = v.findViewById(R.id.et_task_desc); btnDate = v.findViewById(R.id.btn_task_date); btnClearDate = v.findViewById(R.id.btn_clear_date); // NYTT btnUsers = v.findViewById(R.id.btn_task_users); btnSave = v.findViewById(R.id.btn_save_task); txtDatePreview = v.findViewById(R.id.txt_date_preview); txtUsersPreview = v.findViewById(R.id.txt_users_preview); if (taskToEdit != null) { txtSheetTitle.setText("Rediger Oppgave"); etTitle.setText(taskToEdit.getTitle()); etDesc.setText(taskToEdit.getDescription()); if (taskToEdit.getDueDate() > 0) { dueDate.setTimeInMillis(taskToEdit.getDueDate()); hasDate = true; } else { hasDate = false; } btnSave.setText("Oppdater Oppgave"); } else { dueDate.add(Calendar.DAY_OF_MONTH, 1); hasDate = true; } updateDatePreview(); btnDate.setOnClickListener(view -> { new DatePickerDialog(getContext(), (d, y, m, day) -> { dueDate.set(y, m, day); hasDate = true; updateDatePreview(); }, dueDate.get(Calendar.YEAR), dueDate.get(Calendar.MONTH), dueDate.get(Calendar.DAY_OF_MONTH)).show(); }); // NYTT: Knapp for å fjerne frist btnClearDate.setOnClickListener(v1 -> { hasDate = false; updateDatePreview(); }); btnUsers.setOnClickListener(view -> showUserSelectionDialog()); btnSave.setOnClickListener(view -> saveTask()); fetchAndFilterUsers(); return v; } private void fetchAndFilterUsers() { RetrofitClient.getApiService().getUsersList().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { filteredUsers = UserFilterHelper.getFilteredUsers(response.body()); if (taskToEdit != null) { selectedUsers.clear(); Map currentAssignees = taskToEdit.getAssigneeStatus(); for (User u : filteredUsers) { if (currentAssignees.containsKey(u.getEmail())) { selectedUsers.add(u); } } updateUsersPreview(); } } } @Override public void onFailure(Call> call, Throwable t) {} }); } private void showUserSelectionDialog() { if (filteredUsers.isEmpty()) { Toast.makeText(getContext(), "Henter tilgjengelige personer...", Toast.LENGTH_SHORT).show(); return; } String[] names = new String[filteredUsers.size()]; boolean[] checked = new boolean[filteredUsers.size()]; for (int i = 0; i < filteredUsers.size(); i++) { names[i] = filteredUsers.get(i).getName(); boolean isSelected = false; for(User su : selectedUsers) { if(su.getEmail().equalsIgnoreCase(filteredUsers.get(i).getEmail())) { isSelected = true; break; } } checked[i] = isSelected; } new AlertDialog.Builder(getContext()) .setTitle("Tildel til...") .setMultiChoiceItems(names, checked, (dialog, which, isChecked) -> { User user = filteredUsers.get(which); if (isChecked) { selectedUsers.add(user); } else { selectedUsers.removeIf(u -> u.getEmail().equalsIgnoreCase(user.getEmail())); } }) .setPositiveButton("OK", (dialog, which) -> updateUsersPreview()) .show(); } private void updateUsersPreview() { if (selectedUsers.isEmpty()) txtUsersPreview.setText("Kun meg"); else if (selectedUsers.size() == 1) txtUsersPreview.setText(selectedUsers.get(0).getName()); else txtUsersPreview.setText(selectedUsers.size() + " personer valgt"); } private void updateDatePreview() { if (hasDate) { SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime())); btnClearDate.setVisibility(View.VISIBLE); } else { txtDatePreview.setText("Ingen frist"); btnClearDate.setVisibility(View.GONE); } } private void saveTask() { String title = etTitle.getText().toString().trim(); if (title.isEmpty()) { etTitle.setError("Mangler tittel"); return; } long finalDueDate = hasDate ? dueDate.getTimeInMillis() : 0; if (taskToEdit != null) { Map oldStatus = taskToEdit.getAssigneeStatus(); taskToEdit.getAssigneeStatus().clear(); if (selectedUsers.isEmpty()) { String myEmail = UserManager.getInstance().getUserEmail(); taskToEdit.addAssignee(myEmail); if (oldStatus.containsKey(myEmail)) taskToEdit.setParticipantStatus(myEmail, oldStatus.get(myEmail)); } else { for (User u : selectedUsers) { taskToEdit.addAssignee(u.getEmail()); if (oldStatus.containsKey(u.getEmail())) { taskToEdit.setParticipantStatus(u.getEmail(), oldStatus.get(u.getEmail())); } } } taskToEdit.setTitle(title); taskToEdit.setDescription(etDesc.getText().toString()); taskToEdit.setDueDate(finalDueDate); if (listener != null) listener.onTaskUpdated(taskToEdit); } else { TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), finalDueDate); if (selectedUsers.isEmpty()) { newTask.addAssignee(UserManager.getInstance().getUserEmail()); } else { for (User u : selectedUsers) newTask.addAssignee(u.getEmail()); } if (listener != null) listener.onTaskAdded(newTask); } dismiss(); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java ============================================================ package com.kbs.kbsintranett; import android.Manifest; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; 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 { private static final String CHANNEL_ID = "kbs_calendar_channel"; private static final String CHANNEL_NAME = "KBS Kalendervarsler"; @Override public void onReceive(Context context, Intent intent) { String title = intent.getStringExtra("TITLE"); String message = intent.getStringExtra("MESSAGE"); int notificationId = intent.getIntExtra("ID", 0); createNotificationChannel(context); showNotification(context, title, message, notificationId); } private void showNotification(Context context, String title, String message, int notificationId) { // Sjekk rettigheter for Android 13+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // Vi kan ikke vise varsel uten rettighet. return; } } // Intent for hva som skjer når man trykker på varselet (åpne appen) Intent tapIntent = new Intent(context, MainActivity.class); tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( context, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) .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) .setContentIntent(pendingIntent) .setAutoCancel(true); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notificationId, builder.build()); } private void createNotificationChannel(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int importance = NotificationManager.IMPORTANCE_HIGH; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance); channel.setDescription("Varsler for kalenderhendelser i KBS Intranett"); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); if (notificationManager != null) { notificationManager.createNotificationChannel(channel); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmScheduler.java ============================================================ package com.kbs.kbsintranett; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.util.Log; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; public class AlarmScheduler { private static final String TAG = "AlarmScheduler"; private static final String PREFS_NAME = "kbs_alarm_history"; /** * Denne metoden går gjennom en liste hendelser og setter alarmer for dem. */ public static void scheduleAlarmsForEvents(Context context, List events) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); // Sjekk rettigheter for Android 12+ (Exact Alarm) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { Log.w(TAG, "Mangler rettighet til å sette nøyaktige alarmer."); return; } SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); long now = System.currentTimeMillis(); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); // Vi ser etter hendelser 30 dager frem i tid long futureWindow = now + (30L * 24 * 60 * 60 * 1000L); for (CalendarEvent event : events) { try { // Hopp over hvis ingen dato eller heldags (uten tidspunkt) if (event.getRawDate() == null || event.getRawDate().length() == 10) continue; Date eventDate = null; if (event.getRawDate().contains("T")) { String raw = event.getRawDate(); if (raw.length() > 19) raw = raw.substring(0, 19); eventDate = isoFormat.parse(raw); } if (eventDate == null) continue; // Loop gjennom alle varsler (f.eks. 15 min før, 60 min før) for (int minutesBefore : event.getReminders()) { if (minutesBefore < 0) continue; long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L); String alarmKey = "alarm_" + event.getId() + "_" + triggerTime; // Hvis tidspunktet er i fremtiden (og innenfor vinduet) if (triggerTime > now && triggerTime < futureWindow) { // Sjekk om vi allerede har satt denne alarmen for å unngå dobbeltarbeid if (prefs.getBoolean(alarmKey, false)) { continue; } int alarmId = alarmKey.hashCode(); // Unik ID basert på hendelse+tid Intent intent = new Intent(context, AlarmReceiver.class); intent.putExtra("TITLE", event.getTitle()); String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate); intent.putExtra("MESSAGE", "Starter kl " + timeStr); intent.putExtra("ID", alarmId); PendingIntent pendingIntent = PendingIntent.getBroadcast( context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); // Sett alarmen if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } // Marker som satt prefs.edit().putBoolean(alarmKey, true).apply(); Log.d(TAG, "Alarm satt for " + event.getTitle() + " om " + minutesBefore + " min."); } } } catch (Exception e) { Log.e(TAG, "Feil ved setting av alarm", e); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java package com.kbs.kbsintranett; import android.util.Log; import com.google.gson.JsonElement; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class AuthRepository { private static final String TAG = "AuthRepository"; // Interface for å gi beskjed tilbake til Activity/Fragment public interface AuthCallback { void onSuccess(String role); void onError(String message); } /** * Utfører selve API-kallet mot WordPress. * Denne brukes nå av både MainActivity (Silent Sign-In) og LoginFragment (Manuell). */ public static void loginToWordPress(String googleIdToken, String displayName, String email, String photoUrl, AuthCallback callback) { // 1. Lagre Google-info midlertidig UserManager.getInstance().setUserData(displayName, email, googleIdToken, photoUrl); // 2. Gjør klar request LoginRequest request = new LoginRequest(googleIdToken); // 3. Send til WordPress RetrofitClient.getApiService().googleLogin(request).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null && response.body().success) { // SUKSESS! String cookie = response.body().fullCookie; String role = response.body().role; // NYTT: Hent utvidet info fra responsen int userId = response.body().userId; String fName = response.body().firstName; String lName = response.body().lastName; String stilling = response.body().stilling; String mobil = response.body().mobiltelefon; Log.d(TAG, "WordPress Login suksess! Rolle: " + role + ", UserID: " + userId); // Lagre cookie, rolle og ID UserManager.getInstance().setCookie(cookie); UserManager.getInstance().setUserRole(role); UserManager.getInstance().setUserId(userId); // Lagre utvidet info i UserManager UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil); // Lagre listen over skrivbare kalendere UserManager.getInstance().setWriteableCalendars(response.body().writeableCalendars); // NYTT: Hvis vi har en ventende FCM-token, send den nå som vi er logget inn String pendingToken = UserManager.getInstance().getFcmToken(); if (pendingToken != null && !pendingToken.isEmpty()) { updateDeviceToken(pendingToken); } callback.onSuccess(role); } else { Log.e(TAG, "WordPress Login nektet. Kode: " + response.code()); callback.onError("Kunne ikke logge inn på Intranettet (Kode: " + response.code() + ")"); } } @Override public void onFailure(Call call, Throwable t) { Log.e(TAG, "Nettverksfeil mot WP", t); callback.onError("Nettverksfeil: " + t.getMessage()); } }); } /** * Sender FCM-token til WordPress for å registrere enheten for push-varsler. */ public static void updateDeviceToken(String token) { if (!UserManager.getInstance().isLoggedIn()) { // Hvis ikke logget inn, bare lagre den til senere UserManager.getInstance().setFcmToken(token); return; } // Send til server RegisterDeviceRequest request = new RegisterDeviceRequest(token); RetrofitClient.getApiService().registerDevice(request).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { Log.d(TAG, "FCM Token registrert på server OK."); } else { Log.e(TAG, "Feil ved registrering av FCM Token. Kode: " + response.code()); } } @Override public void onFailure(Call call, Throwable t) { Log.e(TAG, "Nettverksfeil ved sending av FCM token", t); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CacheManager.java ============================================================ package com.kbs.kbsintranett; import android.content.Context; import android.content.SharedPreferences; // NYTT import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; 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 FILE_TASKS = "cache_tasks.json"; private static final String TAG = "CacheManager"; private static final Gson gson = new Gson(); // NYTT: SharedPreferences for tidsstempler private static final String PREFS_CACHE = "kbs_cache_prefs"; // --- KALENDER --- public static void saveCalendarEvents(Context context, List events) { saveList(context, FILE_CALENDAR, events); setLastFetchTime(context, "calendar"); // NYTT } public static List getCachedCalendarEvents(Context context) { 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); setLastFetchTime(context, "news"); // NYTT } 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); // Håndboken endres sjelden, trenger kanskje ikke tidssjekk, men greit å ha } public static List getCachedHandbookItems(Context context) { Type type = new TypeToken>() {}.getType(); List list = loadList(context, FILE_HANDBOOK, type); return list != null ? list : new ArrayList<>(); } // --- OPPGAVER --- public static void saveTasks(Context context, List tasks) { saveList(context, FILE_TASKS, tasks); setLastFetchTime(context, "tasks"); // NYTT } public static List getTasks(Context context) { Type type = new TypeToken>() {}.getType(); List list = loadList(context, FILE_TASKS, type); return list != null ? list : new ArrayList<>(); } // --- LOGIKK FOR TID --- private static void setLastFetchTime(Context context, String key) { SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); prefs.edit().putLong(key + "_timestamp", System.currentTimeMillis()).apply(); } /** * Sjekker om cachen er gyldig. * @param maxAgeMinutes Hvor gammel cachen kan være før vi henter på nytt (f.eks. 60 minutter). * Hvis push fungerer, kan vi sette denne høyt! */ public static boolean isCacheValid(Context context, String key, int maxAgeMinutes) { SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); long lastTime = prefs.getLong(key + "_timestamp", 0); long diff = System.currentTimeMillis() - lastTime; // Konverter minutter til millisekunder long maxDiff = maxAgeMinutes * 60 * 1000L; return diff < maxDiff; } // Metode for å tvinge oppdatering neste gang (brukes av FCM) public static void invalidateCache(Context context, String key) { SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE); prefs.edit().remove(key + "_timestamp").apply(); } // --- GENERISKE HJELPEMETODER (Uendret) --- private static void saveList(Context context, String filename, List list) { if (context == null || list == null) return; new Thread(() -> { try { String json = gson.toJson(list); FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); fos.write(json.getBytes()); fos.close(); } 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; import java.util.List; public class CalendarAdapter extends RecyclerView.Adapter { public static final int TYPE_EVENT = 0; public static final int TYPE_YEAR_HEADER = 1; private List items; private final OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(CalendarEvent event); } public CalendarAdapter(List items, OnItemClickListener listener) { this.items = items; this.listener = listener; } @Override public int getItemViewType(int position) { return (items.get(position) instanceof String) ? TYPE_YEAR_HEADER : TYPE_EVENT; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (viewType == TYPE_YEAR_HEADER) { return new YearViewHolder(inflater.inflate(R.layout.item_calendar_year_header, parent, false)); } else { return new EventViewHolder(inflater.inflate(R.layout.item_calendar, parent, false)); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { Object item = items.get(position); if (holder instanceof YearViewHolder) { ((YearViewHolder) holder).yearText.setText((String) item); } else if (holder instanceof EventViewHolder) { CalendarEvent event = (CalendarEvent) item; EventViewHolder vh = (EventViewHolder) holder; vh.day.setText(event.getDay()); vh.month.setText(event.getMonth()); vh.time.setText(event.getTime()); vh.title.setText(event.getTitle()); // --- ÅRSTALL LOGIKK: BAKGRUNNSFARGE --- // Vi henter årstall fra datoen (yyyy-MM-dd) String year = "2025"; if (event.getRawDate() != null && event.getRawDate().length() >= 4) { year = event.getRawDate().substring(0, 4); } // Alternerende bakgrunn basert på år (partall vs oddetall) int yearInt = Integer.parseInt(year); if (yearInt % 2 == 0) { vh.itemView.setBackgroundColor(Color.parseColor("#F5F7FA")); // KBS Very Light Blue } else { vh.itemView.setBackgroundColor(Color.WHITE); } // Privat-markering og farge på datoboks boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:"); try { int color = Color.parseColor(isPrivate ? "#673AB7" : event.getCalendarColor()); vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(color)); } catch (Exception e) { vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#0069B3"))); } vh.itemView.setOnClickListener(v -> listener.onItemClick(event)); } } @Override public int getItemCount() { return items.size(); } static class EventViewHolder extends RecyclerView.ViewHolder { TextView day, month, title, time; LinearLayout dateBox; EventViewHolder(View view) { super(view); day = view.findViewById(R.id.cal_day); month = view.findViewById(R.id.cal_month); title = view.findViewById(R.id.cal_title); time = view.findViewById(R.id.cal_time); dateBox = view.findViewById(R.id.date_box_background); } } static class YearViewHolder extends RecyclerView.ViewHolder { TextView yearText; YearViewHolder(View view) { super(view); yearText = view.findViewById(R.id.txt_calendar_year); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarDetailsBottomSheet.java ============================================================ package com.kbs.kbsintranett; import android.app.AlertDialog; import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.text.Html; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.navigation.fragment.NavHostFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.gson.JsonElement; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { private CalendarEvent event; private OnEventChangeListener changeListener; private static final String PRIVATE_EVENT_COLOR = "#673AB7"; public interface OnEventChangeListener { void onEventChanged(); } public CalendarDetailsBottomSheet(CalendarEvent event) { this.event = event; } public void setOnEventChangeListener(OnEventChangeListener listener) { this.changeListener = listener; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.bottom_sheet_calendar_details, container, false); TextView title = view.findViewById(R.id.sheet_title); TextView time = view.findViewById(R.id.sheet_time); TextView desc = view.findViewById(R.id.sheet_desc); TextView loc = view.findViewById(R.id.sheet_location); TextView calName = view.findViewById(R.id.sheet_calendar_name); TextView participantsView = view.findViewById(R.id.sheet_participants); TextView organizerView = view.findViewById(R.id.sheet_organizer); // NYTT LinearLayout adminLayout = view.findViewById(R.id.layout_admin_buttons); Button btnDelete = view.findViewById(R.id.btn_delete); Button btnEdit = view.findViewById(R.id.btn_edit); title.setText(event.getTitle()); time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")"); // Sjekk om privat boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:"); if (isPrivate) { calName.setText(event.getCalendarName().toUpperCase() + " (BEGRENSET INNSYN)"); try { calName.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor(PRIVATE_EVENT_COLOR))); } catch (Exception e) {} showParticipants(event.getDescription(), participantsView); } else { calName.setText(event.getCalendarName().toUpperCase()); try { int color = Color.parseColor(event.getCalendarColor()); calName.setBackgroundTintList(ColorStateList.valueOf(color)); } catch (Exception e) {} participantsView.setVisibility(View.GONE); } // VIS ARRANGØR ALLTID showOrganizer(event.getDescription(), organizerView); // --- BESKRIVELSE OG LENKER --- if (!event.getDescription().isEmpty()) { String cleanDesc = event.getDescription() .replaceAll("#varsel:[\\d,]+", "") .replaceAll("#deltakere:[^\\s]+", "") .replaceAll("#arrangor:.+", "") // Fjern arrangør fra brødtekst .trim(); if (!cleanDesc.isEmpty()) { desc.setText(Html.fromHtml(cleanDesc, Html.FROM_HTML_MODE_COMPACT)); Linkify.addLinks(desc, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS); desc.setMovementMethod(LinkMovementMethod.getInstance()); desc.setVisibility(View.VISIBLE); } else { desc.setVisibility(View.GONE); } } else { desc.setVisibility(View.GONE); } // --- ADRESSE OG KART --- if (!event.getLocation().isEmpty()) { loc.setText(event.getLocation()); loc.setVisibility(View.VISIBLE); loc.setOnClickListener(v -> { String location = event.getLocation(); Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + Uri.encode(location)); Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); mapIntent.setPackage("com.google.android.apps.maps"); try { startActivity(mapIntent); } catch (Exception e) { mapIntent.setPackage(null); try { startActivity(mapIntent); } catch (Exception ex) { Toast.makeText(getContext(), "Fant ingen kart-app", Toast.LENGTH_SHORT).show(); } } }); } else { loc.setVisibility(View.GONE); } // Sjekk admin-rettigheter if (UserManager.getInstance().isEditorOrAbove()) { boolean canEdit = UserManager.getInstance().getWriteableCalendars().contains(event.getCalendarName()) || UserManager.getInstance().isAdmin(); 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; } private void showParticipants(String rawDescription, TextView view) { if (rawDescription == null) return; Matcher m = Pattern.compile("#deltakere:([^\\s]+)").matcher(rawDescription); if (m.find()) { String allEmails = m.group(1); String[] emails = allEmails.split(","); StringBuilder sb = new StringBuilder(); sb.append("Synlig for:
"); for (String email : emails) { sb.append("• ").append(email.trim()).append("
"); } view.setText(Html.fromHtml(sb.toString(), Html.FROM_HTML_MODE_COMPACT)); view.setVisibility(View.VISIBLE); } } // NYTT: Vis arrangør private void showOrganizer(String rawDescription, TextView view) { if (rawDescription == null) return; Matcher m = Pattern.compile("#arrangor:(.+)").matcher(rawDescription); if (m.find()) { String organizer = m.group(1).trim(); view.setText("Invitert av: " + organizer); view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } private void confirmDelete() { new AlertDialog.Builder(getContext()) .setTitle("Slett hendelse") .setMessage("Er du sikker på at du vil slette '" + event.getTitle() + "'?") .setPositiveButton("Slett", (dialog, which) -> deleteEvent()) .setNegativeButton("Avbryt", null) .show(); } private void deleteEvent() { CreateEventRequest req = new CreateEventRequest( "", "", "", "", "", event.getCalendarName(), new ArrayList<>(), false, "" ); req.id = event.getId(); RetrofitClient.getApiService().deleteCalendarEvent(req).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { Toast.makeText(getContext(), "Slettet!", Toast.LENGTH_SHORT).show(); if (changeListener != null) { changeListener.onEventChanged(); } dismiss(); } else { Toast.makeText(getContext(), "Kunne ikke slette", Toast.LENGTH_LONG).show(); } } @Override public void onFailure(Call call, Throwable t) { Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class CalendarEvent implements Serializable { @SerializedName("id") private String id; @SerializedName("title") private String title; @SerializedName("start_date") private String rawDate; @SerializedName("end_date") private String rawEndDate; @SerializedName("description") private String description; @SerializedName("location") private String location; @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; private String time; // Konstruktør public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) { this.title = title; this.rawDate = rawDate; this.rawEndDate = rawEndDate; this.description = description; this.location = location; } public String getId() { return id; } public String getTitle() { return title; } public String getRawDate() { return rawDate; } public String getRawEndDate() { return rawEndDate; } public String getDescription() { return description != null ? description : ""; } public String getLocation() { return location != null ? location : ""; } public List getReminders() { return reminders != null ? reminders : new ArrayList<>(); } // NYE GETTERS/SETTERS public String getCalendarName() { return calendarName != null ? calendarName : "Ukjent"; } public void setCalendarName(String name) { this.calendarName = name; } public String getCalendarColor() { return calendarColor != null ? calendarColor : "#888888"; } public void setCalendarColor(String color) { this.calendarColor = color; } // --- KOMPATIBILITETS-METODER --- // Denne brukes for enkle varsler public void setReminderMinutes(int minutes) { this.reminders = new ArrayList<>(); if (minutes > 0) { this.reminders.add(minutes); } } // NY METODE (Den som manglet og forårsaket krasj) // Lar oss sette hele listen med varsler på en gang public void setRemindersList(List reminders) { this.reminders = reminders != null ? new ArrayList<>(reminders) : new ArrayList<>(); } public int getReminderMinutes() { if (reminders != null && !reminders.isEmpty()) { return reminders.get(0); } return 0; } // --- UI SETTERS/GETTERS --- public String getDay() { return day; } public void setDay(String day) { this.day = day; } public String getMonth() { return month; } public void setMonth(String month) { this.month = month; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } } ============================================================ 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; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class CalendarFullFragment extends Fragment { private RecyclerView recyclerView; private ProgressBar progressBar; private TextView emptyView; private FloatingActionButton fabAddEvent; private LinearLayoutManager layoutManager; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_calendar_full, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); recyclerView = view.findViewById(R.id.recycler_full_calendar); progressBar = view.findViewById(R.id.loading_full_calendar); emptyView = view.findViewById(R.id.empty_view_calendar); fabAddEvent = view.findViewById(R.id.fab_add_calendar_event); layoutManager = new LinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager); if (!UserManager.getInstance().getWriteableCalendars().isEmpty() || UserManager.getInstance().isEditorOrAbove()) { fabAddEvent.setVisibility(View.VISIBLE); fabAddEvent.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_create_event)); } } @Override public void onResume() { super.onResume(); fetchAllEvents(); } private void fetchAllEvents() { progressBar.setVisibility(View.VISIBLE); new Thread(() -> { 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) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { apiEvents = response.body(); for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); } List allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); displaySortedList(allEvents); } @Override public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); displaySortedList(deviceEvents); } }); } private void displaySortedList(List events) { if (events.isEmpty()) { emptyView.setVisibility(View.VISIBLE); recyclerView.setVisibility(View.GONE); return; } emptyView.setVisibility(View.GONE); recyclerView.setVisibility(View.VISIBLE); List itemsWithHeaders = new ArrayList<>(); String currentYear = ""; for (CalendarEvent e : events) { String year = "Ukjent år"; if (e.getRawDate() != null && e.getRawDate().length() >= 4) { year = e.getRawDate().substring(0, 4); } if (!year.equals(currentYear)) { itemsWithHeaders.add(year); currentYear = year; } itemsWithHeaders.add(e); } CalendarAdapter adapter = new CalendarAdapter(itemsWithHeaders, event -> { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); sheet.setOnEventChangeListener(CalendarFullFragment.this::fetchAllEvents); sheet.show(getParentFragmentManager(), "CalendarDetails"); }); recyclerView.setAdapter(adapter); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarManager.java ============================================================ package com.kbs.kbsintranett; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.provider.CalendarContract; import androidx.core.content.ContextCompat; 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 { private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { return ids; } String[] projection = new String[] { CalendarContract.Calendars._ID, CalendarContract.Calendars.ACCOUNT_NAME }; String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; String[] selectionArgs = new String[] {"%@kbs.no"}; try (Cursor cursor = context.getContentResolver().query( CalendarContract.Calendars.CONTENT_URI, projection, selection, selectionArgs, null )) { if (cursor != null) { while (cursor.moveToNext()) { ids.add(cursor.getString(0)); } } } catch (Exception e) { e.printStackTrace(); } return ids; } /** * 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) { return deviceEvents; } List kbsCalendarIds = getKbsCalendarIds(context); if (kbsCalendarIds.isEmpty()) { return deviceEvents; } long now = System.currentTimeMillis(); 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, CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND, CalendarContract.Events.DESCRIPTION, CalendarContract.Events.EVENT_LOCATION, CalendarContract.Events.ALL_DAY }; StringBuilder selection = new StringBuilder("("); List selectionArgsList = new ArrayList<>(); for (int i = 0; i < kbsCalendarIds.size(); i++) { selection.append(CalendarContract.Events.CALENDAR_ID).append(" = ?"); selectionArgsList.add(kbsCalendarIds.get(i)); if (i < kbsCalendarIds.size() - 1) { selection.append(" OR "); } } selection.append(") AND "); selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); selectionArgsList.add(String.valueOf(startMillis)); selectionArgsList.add(String.valueOf(endMillis)); String[] selectionArgs = selectionArgsList.toArray(new String[0]); try (Cursor cursor = context.getContentResolver().query( CalendarContract.Events.CONTENT_URI, projection, selection.toString(), selectionArgs, CalendarContract.Events.DTSTART + " ASC" )) { if (cursor != null) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); while (cursor.moveToNext()) { String title = cursor.getString(0); long dtStart = cursor.getLong(1); long dtEnd = cursor.getLong(2); String desc = cursor.getString(3); String loc = cursor.getString(4); int allDay = cursor.getInt(5); String rawStart; String rawEnd; if (allDay == 1) { SimpleDateFormat shortFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); rawStart = shortFormat.format(new Date(dtStart)); rawEnd = shortFormat.format(new Date(dtEnd)); } else { rawStart = isoFormat.format(new Date(dtStart)); rawEnd = isoFormat.format(new Date(dtEnd)); } CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); event.setReminderMinutes(0); event.setCalendarColor("#888888"); event.setCalendarName("Min Kalender (Lokal)"); formatEventForUI(event); deviceEvents.add(event); } } } catch (Exception e) { e.printStackTrace(); } return deviceEvents; } public static void formatEventForUI(CalendarEvent event) { if (event.getRawDate() == null) return; SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); try { Date date = null; Date endDate = null; boolean isAllDay = false; String raw = event.getRawDate(); if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); date = shortFmt.parse(raw); isAllDay = true; if (event.getRawEndDate() != null && event.getRawEndDate().length() == 10) { endDate = shortFmt.parse(event.getRawEndDate()); } } else if (raw.contains("T")) { SimpleDateFormat isoFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); date = isoFmt.parse(raw); if (event.getRawEndDate() != null && event.getRawEndDate().contains("T")) { endDate = isoFmt.parse(event.getRawEndDate()); } } else { SimpleDateFormat sqlFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); sqlFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); date = sqlFmt.parse(raw); if (event.getRawEndDate() != null) { endDate = sqlFmt.parse(event.getRawEndDate()); } } if (date != null) { event.setDay(outputDay.format(date)); event.setMonth(outputMonth.format(date).toUpperCase()); if (isAllDay) { event.setTime("Hele dagen"); } else { String timeStr = outputTime.format(date); if (endDate != null) { timeStr += " - " + outputTime.format(endDate); } event.setTime("Kl. " + timeStr); } } } catch (Exception e) { e.printStackTrace(); event.setDay("??"); event.setMonth("???"); event.setTime("Feil dato"); } } public static List mergeAndSort(List apiEvents, List 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() : ""; return d1.compareTo(d2); }); 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; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CategoryAdapter.java ============================================================ package com.kbs.kbsintranett; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class CategoryAdapter extends RecyclerView.Adapter { private List categories; private String selectedCategory = "Alle"; // Standardvalg private OnCategoryClickListener listener; public interface OnCategoryClickListener { void onCategoryClick(String category); } public CategoryAdapter(List categories, OnCategoryClickListener listener) { this.categories = categories; this.listener = listener; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { String category = categories.get(position); holder.name.setText(category); if (category.equals(selectedCategory)) { // Valgt stil (Blå bakgrunn, hvit tekst) holder.name.setBackgroundResource(R.drawable.bg_category_selected); holder.name.setTextColor(Color.WHITE); } else { // Ikke valgt stil (Hvit bakgrunn, mørk tekst) holder.name.setBackgroundResource(R.drawable.bg_category_unselected); holder.name.setTextColor(Color.parseColor("#333333")); } holder.itemView.setOnClickListener(v -> { selectedCategory = category; notifyDataSetChanged(); // Oppdater alle for å flytte markering listener.onCategoryClick(category); }); } @Override public int getItemCount() { return categories.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView name; public ViewHolder(View view) { super(view); name = view.findViewById(R.id.category_name); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\ChoicesAdapter.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; public class ChoicesAdapter implements JsonDeserializer> { @Override public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { // Hvis feltet er "null" eller en tom tekststreng, returner en tom liste if (json.isJsonNull() || (json.isJsonPrimitive() && json.getAsString().isEmpty())) { return new ArrayList<>(); } // Hvis det faktisk er en liste (Array), les den som vanlig if (json.isJsonArray()) { List list = new ArrayList<>(); for (JsonElement e : json.getAsJsonArray()) { list.add(context.deserialize(e, GravityField.Choice.class)); } return list; } // Hvis vi får noe annet rart (f.eks. en tekst som ikke er tom), ignorer det for å unngå krasj return new ArrayList<>(); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\ConditionalLogicAdapter.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import java.lang.reflect.Type; public class ConditionalLogicAdapter implements JsonDeserializer { @Override public GravityField.ConditionalLogic deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { // Hvis feltet er en streng (f.eks tom streng ""), returner null if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { return null; } // Hvis det er et objekt, bruk standard deserialisering if (json.isJsonObject()) { // Vi må manuelt deserialisere for å unngå uendelig løkke hvis vi bare kaller context.deserialize på samme type // Enkleste måte er å la Gson gjøre jobben på innholdet return new com.google.gson.Gson().fromJson(json, GravityField.ConditionalLogic.class); } return null; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CreateEventFragment.java ============================================================ package com.kbs.kbsintranett; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.gson.JsonElement; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class CreateEventFragment extends Fragment { private EditText etTitle, etDesc, etLocation; private Spinner spinnerCalendar, spinnerRecurrence; private ChipGroup chipGroupReminders; private Switch switchAllDay; private TextView txtPreview, txtSelectedParticipants; private Button btnStartDate, btnStartTime, btnEndDate, btnEndTime, btnSave, btnSelectParticipants; private RadioButton rbAll, rbSpecific; private Calendar startCal = Calendar.getInstance(); private Calendar endCal = Calendar.getInstance(); private String selectedRRule = null; private boolean isCustomRecurrence = false; // BRUKERLISTER private List filteredUsers = new ArrayList<>(); private String originalOrganizer = null; private CalendarEvent eventToEdit = null; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_create_event, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Initialisering av Views etTitle = view.findViewById(R.id.et_title); etDesc = view.findViewById(R.id.et_desc); etLocation = view.findViewById(R.id.et_location); switchAllDay = view.findViewById(R.id.switch_all_day); spinnerCalendar = view.findViewById(R.id.spinner_calendar); spinnerRecurrence = view.findViewById(R.id.spinner_recurrence); chipGroupReminders = view.findViewById(R.id.chip_group_reminders); txtPreview = view.findViewById(R.id.txt_time_preview); txtSelectedParticipants = view.findViewById(R.id.txt_selected_participants); btnStartDate = view.findViewById(R.id.btn_start_date); btnStartTime = view.findViewById(R.id.btn_start_time); btnEndDate = view.findViewById(R.id.btn_end_date); btnEndTime = view.findViewById(R.id.btn_end_time); btnSave = view.findViewById(R.id.btn_save_event); btnSelectParticipants = view.findViewById(R.id.btn_select_participants); rbAll = view.findViewById(R.id.rb_visibility_all); rbSpecific = view.findViewById(R.id.rb_visibility_specific); // Standardtidspunkt (neste time) startCal.add(Calendar.HOUR_OF_DAY, 1); startCal.set(Calendar.MINUTE, 0); endCal.setTime(startCal.getTime()); endCal.add(Calendar.HOUR_OF_DAY, 1); setupCalendarSpinner(); setupReminderChips(); fetchUsers(); // Sjekk om vi redigerer en eksisterende hendelse if (getArguments() != null && getArguments().containsKey("edit_event")) { eventToEdit = (CalendarEvent) getArguments().getSerializable("edit_event"); prefillForm(eventToEdit); btnSave.setText("Oppdater Hendelse"); } updateRecurrenceSpinner(); updateUI(); // Listeners switchAllDay.setOnCheckedChangeListener((btn, isChecked) -> updateUI()); btnStartDate.setOnClickListener(v -> pickDate(startCal, true)); btnEndDate.setOnClickListener(v -> pickDate(endCal, false)); btnStartTime.setOnClickListener(v -> pickTime(startCal)); btnEndTime.setOnClickListener(v -> pickTime(endCal)); btnSave.setOnClickListener(v -> submitEvent()); rbAll.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { btnSelectParticipants.setVisibility(View.GONE); txtSelectedParticipants.setVisibility(View.GONE); } }); rbSpecific.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { btnSelectParticipants.setVisibility(View.VISIBLE); txtSelectedParticipants.setVisibility(View.VISIBLE); } }); btnSelectParticipants.setOnClickListener(v -> showUserSelectionDialog()); spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (eventToEdit != null && position == 0 && selectedRRule != null) return; String selected = parent.getItemAtPosition(position).toString(); if (selected.equals("Egendefinert...")) { showCustomRecurrenceDialog(); } else if (selected.startsWith("Ikke gjenta")) { selectedRRule = null; } else { generateStandardRRule(position); } } @Override public void onNothingSelected(AdapterView parent) {} }); } private void fetchUsers() { RetrofitClient.getApiService().getUsersList().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (!isAdded()) return; if (response.isSuccessful() && response.body() != null) { // BRUKER DEN SENTRALE HJELPEKLASSEN FOR FILTRERING filteredUsers = UserFilterHelper.getFilteredUsers(response.body()); if (eventToEdit != null) { parseParticipantsFromDescription(eventToEdit.getDescription()); } } } @Override public void onFailure(Call> call, Throwable t) {} }); } private void showUserSelectionDialog() { if (filteredUsers.isEmpty()) { Toast.makeText(getContext(), "Ingen personer funnet.", Toast.LENGTH_SHORT).show(); return; } String[] userNames = new String[filteredUsers.size()]; boolean[] checkedItems = new boolean[filteredUsers.size()]; for (int i = 0; i < filteredUsers.size(); i++) { userNames[i] = filteredUsers.get(i).getName(); checkedItems[i] = filteredUsers.get(i).isSelected(); } new AlertDialog.Builder(getContext()) .setTitle("Velg deltakere") .setMultiChoiceItems(userNames, checkedItems, (dialog, which, isChecked) -> { filteredUsers.get(which).setSelected(isChecked); }) .setPositiveButton("OK", (dialog, which) -> updateParticipantPreview()) .setNegativeButton("Avbryt", null) .show(); } private void updateParticipantPreview() { StringBuilder sb = new StringBuilder("Valgte: "); int count = 0; for (User u : filteredUsers) { if (u.isSelected()) { if (count > 0) sb.append(", "); sb.append(u.getName()); count++; } } if (count == 0) txtSelectedParticipants.setText("Ingen valgt"); else txtSelectedParticipants.setText(sb.toString()); } private void parseParticipantsFromDescription(String desc) { if (desc == null) return; Pattern p = Pattern.compile("#deltakere:([^\\s]+)"); Matcher m = p.matcher(desc); if (m.find()) { rbSpecific.setChecked(true); String[] emails = m.group(1).split(","); for (String email : emails) { for (User u : filteredUsers) { if (u.getEmail().equalsIgnoreCase(email.trim())) { u.setSelected(true); } } } updateParticipantPreview(); } else { rbAll.setChecked(true); } } private void prefillForm(CalendarEvent event) { etTitle.setText(event.getTitle()); if (event.getDescription() != null) { Matcher m = Pattern.compile("#arrangor:(.+)").matcher(event.getDescription()); if (m.find()) originalOrganizer = m.group(1).trim(); } String cleanDesc = event.getDescription() .replaceAll("#varsel:[\\d,]+", "") .replaceAll("#deltakere:[^\\s]+", "") .replaceAll("#arrangor:.+", "") .trim(); etDesc.setText(cleanDesc); etLocation.setText(event.getLocation()); 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()); String start = event.getRawDate(); if (start != null) { if (start.length() == 10) { switchAllDay.setChecked(true); startCal.setTime(simpleFormat.parse(start)); if (event.getRawEndDate() != null) { Calendar c = Calendar.getInstance(); c.setTime(simpleFormat.parse(event.getRawEndDate())); c.add(Calendar.DAY_OF_MONTH, -1); endCal.setTime(c.getTime()); } else { endCal.setTime(startCal.getTime()); } } else if (start.contains("T")) { if (start.length() > 19) start = start.substring(0, 19); startCal.setTime(isoFormat.parse(start)); String end = event.getRawEndDate(); if (end != null && end.contains("T")) { if (end.length() > 19) end = end.substring(0, 19); endCal.setTime(isoFormat.parse(end)); } else { endCal.setTime(startCal.getTime()); endCal.add(Calendar.HOUR_OF_DAY, 1); } } } List existingReminders = event.getReminders(); if (!existingReminders.isEmpty()) { for (int i = 0; i < chipGroupReminders.getChildCount(); i++) { Chip chip = (Chip) chipGroupReminders.getChildAt(i); chip.setChecked(existingReminders.contains((Integer) chip.getTag())); } } } catch (Exception e) { Log.e("KBS_EDIT", "Feil ved prefill", e); } } private void setupCalendarSpinner() { List calendars = UserManager.getInstance().getWriteableCalendars(); if (calendars.isEmpty()) { calendars = new ArrayList<>(); calendars.add("Felles"); } ArrayAdapter adapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_item, calendars) { @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { TextView view = (TextView) super.getView(position, convertView, parent); view.setBackgroundColor(Color.parseColor(getCalendarColor(getItem(position)))); view.setTextColor(Color.WHITE); view.setTypeface(null, Typeface.BOLD); return view; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { TextView view = (TextView) super.getDropDownView(position, convertView, parent); view.setTextColor(Color.parseColor(getCalendarColor(getItem(position)))); view.setTypeface(null, Typeface.BOLD); return view; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerCalendar.setAdapter(adapter); } private String getCalendarColor(String name) { if (name == null) return "#888888"; switch (name) { case "Felles": return "#0069B3"; // KBS Blå case "AMU/HMS/Miljø": return "#2E7D32"; // Mørk grønn (Miljø-profil) case "Administrasjonen": return "#607D8B"; case "Serviceavdelingen": return "#E65100"; case "Automasjonsavdelingen": return "#2E7D32"; // Merk: Denne er lik Miljø nå, du kan bytte til f.eks #1B5E20 hvis ønskelig case "Prosjektavdelingen": return "#7B1FA2"; default: return "#888888"; } } private void setupReminderChips() { chipGroupReminders.removeAllViews(); addReminderChip("Ved start", 0); addReminderChip("5 min", 5); addReminderChip("10 min", 10); addReminderChip("15 min", 15); addReminderChip("30 min", 30); addReminderChip("1 t", 60); addReminderChip("2 t", 120); addReminderChip("1 d", 1440); addReminderChip("2 d", 2880); addReminderChip("1 u", 10080); if (eventToEdit == null) { for (int i=0; i options = new ArrayList<>(); options.add("Ikke gjenta"); options.add("Daglig"); options.add("Ukentlig på " + dayName); options.add("Månedlig den " + dayOfMonth + "."); options.add("Månedlig den " + weekNo + ". " + dayName + "en"); options.add("Årlig den " + dayOfMonth + ". " + monthName); options.add("Hver ukedag (man-fre)"); options.add("Egendefinert..."); spinnerRecurrence.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, options)); } private void generateStandardRRule(int position) { switch (position) { case 1: selectedRRule = "RRULE:FREQ=DAILY"; break; case 2: selectedRRule = "RRULE:FREQ=WEEKLY"; break; case 3: selectedRRule = "RRULE:FREQ=MONTHLY"; break; case 4: selectedRRule = "RRULE:FREQ=MONTHLY;BYDAY=" + ((startCal.get(Calendar.DAY_OF_MONTH)-1)/7+1) + getDayCode(startCal.get(Calendar.DAY_OF_WEEK)); break; case 5: selectedRRule = "RRULE:FREQ=YEARLY"; break; case 6: selectedRRule = "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; break; default: selectedRRule = null; } } private String getDayCode(int calendarDay) { switch (calendarDay) { case Calendar.MONDAY: return "MO"; case Calendar.TUESDAY: return "TU"; case Calendar.WEDNESDAY: return "WE"; case Calendar.THURSDAY: return "TH"; case Calendar.FRIDAY: return "FR"; case Calendar.SATURDAY: return "SA"; case Calendar.SUNDAY: return "SU"; default: return "MO"; } } private void showCustomRecurrenceDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_custom_recurrence, null); EditText etInterval = view.findViewById(R.id.et_interval), etCount = view.findViewById(R.id.et_count); Spinner spinnerFreq = view.findViewById(R.id.spinner_freq); View layoutWeekdays = view.findViewById(R.id.layout_weekdays); ToggleButton[] toggles = {view.findViewById(R.id.tg_mon), view.findViewById(R.id.tg_tue), view.findViewById(R.id.tg_wed), view.findViewById(R.id.tg_thu), view.findViewById(R.id.tg_fri), view.findViewById(R.id.tg_sat), view.findViewById(R.id.tg_sun)}; String[] labels = {"M", "T", "O", "T", "F", "L", "S"}; for(int i=0; i<7; i++) { toggles[i].setText(labels[i]); toggles[i].setTextOn(labels[i]); toggles[i].setTextOff(labels[i]); } RadioGroup rgEnd = view.findViewById(R.id.rg_end); Button btnEndDatePicker = view.findViewById(R.id.btn_end_date_picker); Calendar customEndCal = Calendar.getInstance(); customEndCal.add(Calendar.MONTH, 1); btnEndDatePicker.setText(new SimpleDateFormat("d. MMM yyyy", Locale.getDefault()).format(customEndCal.getTime())); btnEndDatePicker.setOnClickListener(v -> { rgEnd.check(R.id.rb_date); new DatePickerDialog(getContext(), (p, y, m, d) -> { customEndCal.set(y, m, d); btnEndDatePicker.setText(new SimpleDateFormat("d. MMM yyyy", Locale.getDefault()).format(customEndCal.getTime())); }, customEndCal.get(Calendar.YEAR), customEndCal.get(Calendar.MONTH), customEndCal.get(Calendar.DAY_OF_MONTH)).show(); }); spinnerFreq.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView p, View v, int pos, long id) { layoutWeekdays.setVisibility(pos == 1 ? View.VISIBLE : View.GONE); } @Override public void onNothingSelected(AdapterView p) {} }); builder.setView(view); AlertDialog dialog = builder.create(); view.findViewById(R.id.btn_cancel).setOnClickListener(v -> { dialog.dismiss(); spinnerRecurrence.setSelection(0); }); view.findViewById(R.id.btn_done).setOnClickListener(v -> { StringBuilder rrule = new StringBuilder("RRULE:FREQ="); rrule.append(new String[]{"DAILY", "WEEKLY", "MONTHLY", "YEARLY"}[spinnerFreq.getSelectedItemPosition()]); if (!etInterval.getText().toString().isEmpty() && !etInterval.getText().toString().equals("1")) rrule.append(";INTERVAL=").append(etInterval.getText().toString()); if (spinnerFreq.getSelectedItemPosition() == 1) { List days = new ArrayList<>(); String[] codes = {"MO", "TU", "WE", "TH", "FR", "SA", "SU"}; for (int i=0; i<7; i++) if (toggles[i].isChecked()) days.add(codes[i]); if (!days.isEmpty()) rrule.append(";BYDAY=").append(String.join(",", days)); } if (rgEnd.getCheckedRadioButtonId() == R.id.rb_date) { customEndCal.set(Calendar.HOUR_OF_DAY, 23); customEndCal.set(Calendar.MINUTE, 59); SimpleDateFormat utc = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); utc.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); rrule.append(";UNTIL=").append(utc.format(customEndCal.getTime())); } else if (rgEnd.getCheckedRadioButtonId() == R.id.rb_count) rrule.append(";COUNT=").append(etCount.getText().toString()); selectedRRule = rrule.toString(); isCustomRecurrence = true; dialog.dismiss(); }); dialog.show(); } private void pickDate(Calendar cal, boolean isStart) { new DatePickerDialog(getContext(), (view, y, m, d) -> { cal.set(y, m, d); if (isStart) { if (endCal.before(startCal)) { endCal.setTime(startCal.getTime()); if (!switchAllDay.isChecked()) endCal.add(Calendar.HOUR_OF_DAY, 1); } updateRecurrenceSpinner(); if (!isCustomRecurrence) spinnerRecurrence.setSelection(0); } updateUI(); }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show(); } private void pickTime(Calendar cal) { new TimePickerDialog(getContext(), (view, h, m) -> { cal.set(Calendar.HOUR_OF_DAY, h); cal.set(Calendar.MINUTE, m); if (cal == startCal && endCal.before(startCal)) { endCal.setTime(startCal.getTime()); endCal.add(Calendar.HOUR_OF_DAY, 1); } updateUI(); }, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), true).show(); } private void updateUI() { boolean isAllDay = switchAllDay.isChecked(); btnStartTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE); btnEndTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE); SimpleDateFormat dFmt = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()), tFmt = new SimpleDateFormat("HH:mm", Locale.getDefault()); btnStartDate.setText(dFmt.format(startCal.getTime())); btnEndDate.setText(dFmt.format(endCal.getTime())); btnStartTime.setText(tFmt.format(startCal.getTime())); btnEndTime.setText(tFmt.format(endCal.getTime())); String p = dFmt.format(startCal.getTime()) + (isAllDay ? "" : " " + tFmt.format(startCal.getTime())) + " - "; if (startCal.get(Calendar.YEAR) == endCal.get(Calendar.YEAR) && startCal.get(Calendar.DAY_OF_YEAR) == endCal.get(Calendar.DAY_OF_YEAR)) { p += (isAllDay ? "(Samme dag)" : tFmt.format(endCal.getTime())); } else p += dFmt.format(endCal.getTime()) + (isAllDay ? "" : " " + tFmt.format(endCal.getTime())); txtPreview.setText(p); } private void submitEvent() { String title = etTitle.getText().toString().trim(); if (title.isEmpty()) { etTitle.setError("Mangler tittel"); return; } SimpleDateFormat sdf = new SimpleDateFormat(switchAllDay.isChecked() ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm", Locale.getDefault()); List reminders = new ArrayList<>(); for (int i=0; i 0) desc += "\n\n#deltakere:" + sb.toString(); } desc += "\n#arrangor:" + (originalOrganizer != null ? originalOrganizer : UserManager.getInstance().getUserDisplayName()); String calSlug = spinnerCalendar.getSelectedItem() != null ? spinnerCalendar.getSelectedItem().toString() : "Felles"; CreateEventRequest req = new CreateEventRequest(title, desc, etLocation.getText().toString(), sdf.format(startCal.getTime()), sdf.format(endCal.getTime()), calSlug, reminders, switchAllDay.isChecked(), selectedRRule); if (eventToEdit != null) req.id = eventToEdit.getId(); Call call = (eventToEdit != null) ? RetrofitClient.getApiService().updateCalendarEvent(req) : RetrofitClient.getApiService().createCalendarEvent(req); call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { Toast.makeText(getContext(), "Lagret!", Toast.LENGTH_LONG).show(); Navigation.findNavController(getView()).navigateUp(); } } @Override public void onFailure(Call call, Throwable t) { Toast.makeText(getContext(), "Feil!", Toast.LENGTH_SHORT).show(); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\CreateEventRequest.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; public class CreateEventRequest { @SerializedName("id") public String id; @SerializedName("title") public String title; @SerializedName("description") public String description; @SerializedName("location") public String location; @SerializedName("start_time") public String startTime; @SerializedName("end_time") public String endTime; @SerializedName("calendar_type") public String calendarType; @SerializedName("reminders") public List reminders; // Liste, ikke int @SerializedName("is_all_day") public boolean isAllDay; @SerializedName("recurrence") public String recurrence; // Oppdatert konstruktør som tar imot List public CreateEventRequest(String title, String description, String location, String startTime, String endTime, String calendarType, List reminders, boolean isAllDay, String recurrence) { this.title = title; this.description = description; this.location = location; this.startTime = startTime; this.endTime = endTime; this.calendarType = calendarType; this.reminders = reminders; this.isAllDay = isAllDay; this.recurrence = recurrence; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsFragment.java ============================================================ package com.kbs.kbsintranett; import android.Manifest; import android.animation.LayoutTransition; import android.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.OpenableColumns; import android.text.Editable; import android.text.Html; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; 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 org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.BufferedSink; import okio.Okio; import okio.Source; public class FormsFragment extends Fragment { private static final String TAG = "FormsFragment"; private static final String BASE_URL_GF = "https://intranet.kbs.no/wp-json/gf/v2"; // SKJEMA ID-er private static final int ID_ANSATTEOPPLYSNINGER = 1; private static final int ID_RUH = 4; private static final int ID_SIKKERHETSKURS = 9; private static final int ID_HMS_BEKREFTELSE = 10; private static final int ID_EGENMELDING = 11; private static final int ID_SJA = 14; private static final int ID_FRAVARSVARSEL = 15; private static final int ID_REFUSJON_UTLEGG = 16; private int formId = 1; private LinearLayout formContainer; private LinearLayout historyContainer; 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<>(); private Map inputViews = new HashMap<>(); private Map requiredFieldsMap = new HashMap<>(); private Map fileUploads = new HashMap<>(); // --- NESTED FORM (BARN) STATE --- private Map childInputViews = new HashMap<>(); private Map childRequiredFieldsMap = new HashMap<>(); private Map childFileUploads = new HashMap<>(); private List nestedEntries = new ArrayList<>(); private LinearLayout nestedEntriesContainer; private TextView totalAmountView; // --- FILOPPLASTING & KAMERA --- private String pendingFileFieldId = null; private boolean isSelectingForChild = false; private Uri currentPhotoUri = null; private ActivityResultLauncher filePickerLauncher; private ActivityResultLauncher takePictureLauncher; private ActivityResultLauncher requestPermissionLauncher; private GravityForm currentForm; private final OkHttpClient client = new OkHttpClient(); private static final Pattern TITLE_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)"); @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); filePickerLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); if (uri != null && pendingFileFieldId != null) { handleFileSelection(pendingFileFieldId, uri, isSelectingForChild); } } } ); takePictureLauncher = registerForActivityResult( new ActivityResultContracts.TakePicture(), success -> { if (success && currentPhotoUri != null && pendingFileFieldId != null) { handleFileSelection(pendingFileFieldId, currentPhotoUri, isSelectingForChild); } else if (!success) { currentPhotoUri = null; } } ); requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { openCamera(); } else { Toast.makeText(getContext(), "Kameratillatelse er påkrevd for å ta bilde.", Toast.LENGTH_LONG).show(); } } ); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_forms, container, false); formContainer = view.findViewById(R.id.form_container); historyContainer = view.findViewById(R.id.historyContainer); historyWrapper = view.findViewById(R.id.history_wrapper); txtStatus = view.findViewById(R.id.txt_status); 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()); } if (view instanceof ViewGroup) { LayoutTransition transition = ((ViewGroup) view).getLayoutTransition(); if (transition == null) { transition = new LayoutTransition(); ((ViewGroup) view).setLayoutTransition(transition); } transition.enableTransitionType(LayoutTransition.CHANGING); } if (formContainer == null) { formContainer = new LinearLayout(getContext()); } if (getArguments() != null) { int argId = getArguments().getInt("formId", 0); if (argId != 0) formId = argId; } fetchFormStructure(); return view; } // --- UI LOGIKK FOR DELT SKJERM --- private void expandFormModule() { if (historyWrapper != null && historyWrapper.getVisibility() == View.VISIBLE) { historyWrapper.setVisibility(View.GONE); if (btnToggleHistory != null) { btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float); } } } private void toggleHistoryVisibility() { if (historyWrapper == null || btnToggleHistory == null) return; if (historyWrapper.getVisibility() == View.VISIBLE) { historyWrapper.setVisibility(View.GONE); btnToggleHistory.setImageResource(android.R.drawable.arrow_down_float); } else { historyWrapper.setVisibility(View.VISIBLE); btnToggleHistory.setImageResource(android.R.drawable.arrow_up_float); } } private void attachInteractionListener(View view) { if (view == null) return; view.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { expandFormModule(); } return false; }); view.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) { expandFormModule(); } }); if (view.isClickable()) { view.setOnClickListener(v -> expandFormModule()); } } // ---------------------------------- private void fetchFormStructure() { if (loadingSpinner != null) loadingSpinner.setVisibility(View.VISIBLE); updateStatus("Laster skjema..."); WordPressApiService api = RetrofitClient.getApiService(); api.getForm(formId).enqueue(new retrofit2.Callback() { @Override public void onResponse(retrofit2.Call call, retrofit2.Response response) { if (response.isSuccessful() && response.body() != null) { currentForm = response.body(); if (getActivity() != null) { getActivity().runOnUiThread(() -> { if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); renderDynamicForm(currentForm); fetchFormEntries(); }); } } else { updateStatus("Feil ved lasting av skjema."); if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); } } @Override public void onFailure(retrofit2.Call call, Throwable t) { updateStatus("Nettverksfeil (Skjema): " + t.getMessage()); if (loadingSpinner != null) loadingSpinner.setVisibility(View.GONE); } }); } private void renderDynamicForm(GravityForm form) { if (formContainer == null) return; formContainer.removeAllViews(); fieldWrappers.clear(); inputViews.clear(); requiredFieldsMap.clear(); fileUploads.clear(); nestedEntries.clear(); updateStatus(""); // 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); } // 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*", ""); formDesc.setText(cleanDesc); formDesc.setPadding(0, 0, 0, 40); formContainer.addView(formDesc); } if (form.fields == null) return; for (GravityField field : form.fields) { if ("hidden".equals(field.type) || field.isHidden || "hidden".equals(field.visibility)) { continue; } LinearLayout fieldWrapper = new LinearLayout(getContext()); fieldWrapper.setOrientation(LinearLayout.VERTICAL); fieldWrapper.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); fieldWrapper.setPadding(0, 10, 0, 20); fieldWrappers.put(String.valueOf(field.id), fieldWrapper); if ("section".equals(field.type)) { addSectionHeader(fieldWrapper, field.label, field.description); formContainer.addView(fieldWrapper); continue; } if ("html".equals(field.type)) { if (field.content != null && !field.content.isEmpty()) { TextView htmlView = new TextView(getContext()); htmlView.setText(Html.fromHtml(field.content, Html.FROM_HTML_MODE_COMPACT)); fieldWrapper.addView(htmlView); } formContainer.addView(fieldWrapper); continue; } TextView label = new TextView(getContext()); String labelText = field.label; if (field.isRequired) labelText += " *"; label.setText(labelText); label.setTextColor(Color.DKGRAY); label.setTypeface(null, Typeface.BOLD); label.setPadding(0, 10, 0, 5); fieldWrapper.addView(label); if ("form".equals(field.type)) { renderNestedFormField(fieldWrapper, field); } else if ("product".equals(field.type) && "calculation".equals(field.inputType)) { renderTotalSumField(fieldWrapper, field); } else if ("time".equals(field.type)) { renderTimeField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("fileupload".equals(field.type)) { renderFileUploadField(fieldWrapper, field, inputViews, requiredFieldsMap, false); } else if (field.inputs != null && !field.inputs.isEmpty()) { if ("consent".equals(field.type)) { renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("checkbox".equals(field.type) || "multi_choice".equals(field.type)) { renderCheckboxField(fieldWrapper, field, inputViews, requiredFieldsMap); } else { renderCompositeField(fieldWrapper, field, inputViews, requiredFieldsMap); } } else if ("radio".equals(field.type)) { renderRadioField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("select".equals(field.type)) { renderSelectField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("textarea".equals(field.type)) { renderTextAreaField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("date".equals(field.type)) { renderDateField(fieldWrapper, field, inputViews, requiredFieldsMap); } else if ("consent".equals(field.type)) { renderConsentField(fieldWrapper, field, inputViews, requiredFieldsMap); } else { renderTextField(fieldWrapper, field, inputViews, requiredFieldsMap); } if (field.description != null && !field.description.isEmpty()) { TextView desc = new TextView(getContext()); desc.setText(Html.fromHtml(field.description, Html.FROM_HTML_MODE_COMPACT)); desc.setTextSize(12); desc.setTextColor(Color.GRAY); fieldWrapper.addView(desc); } formContainer.addView(fieldWrapper); } Button dynamicSubmit = new Button(getContext()); dynamicSubmit.setText("Send inn skjema"); dynamicSubmit.setTextColor(Color.WHITE); dynamicSubmit.setBackgroundColor(Color.parseColor("#0069B3")); // KBS Blå LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins(0, 60, 0, 20); dynamicSubmit.setLayoutParams(params); dynamicSubmit.setOnClickListener(v -> submitDynamicForm()); formContainer.addView(dynamicSubmit); evaluateAllConditionalLogic(); } // --- NESTED FORM LOGIKK --- private void renderNestedFormField(LinearLayout container, GravityField field) { nestedEntriesContainer = new LinearLayout(getContext()); nestedEntriesContainer.setOrientation(LinearLayout.VERTICAL); nestedEntriesContainer.setPadding(0, 10, 0, 10); container.addView(nestedEntriesContainer); Button btnAdd = new Button(getContext()); btnAdd.setText("Legg til vedlegg"); btnAdd.setBackgroundColor(Color.parseColor("#53AFE9")); btnAdd.setTextColor(Color.WHITE); btnAdd.setOnClickListener(v -> { expandFormModule(); int childFormId = 18; if (field.gpnfForm != null) { try { childFormId = Integer.parseInt(field.gpnfForm); } catch (NumberFormatException e) { e.printStackTrace(); } } openChildFormDialog(childFormId, field.id); }); container.addView(btnAdd); EditText hiddenIds = new EditText(getContext()); hiddenIds.setVisibility(View.GONE); inputViews.put(field.id, hiddenIds); } private void renderTotalSumField(LinearLayout container, GravityField field) { totalAmountView = new TextView(getContext()); totalAmountView.setText("Kr 0,00"); totalAmountView.setTextSize(18); totalAmountView.setTypeface(null, Typeface.BOLD); totalAmountView.setPadding(10, 10, 10, 10); container.addView(totalAmountView); } private void openChildFormDialog(int childFormId, String parentFieldId) { if (getActivity() == null) return; ProgressBar pBar = new ProgressBar(getContext()); AlertDialog loadingDialog = new AlertDialog.Builder(getContext()) .setView(pBar) .setMessage("Laster skjema...") .setCancelable(false) .show(); RetrofitClient.getApiService().getForm(childFormId).enqueue(new retrofit2.Callback() { @Override public void onResponse(retrofit2.Call call, retrofit2.Response response) { loadingDialog.dismiss(); if (response.isSuccessful() && response.body() != null) { showChildFormDialog(response.body(), parentFieldId); } else { Toast.makeText(getContext(), "Kunne ikke hente underskjema", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(retrofit2.Call call, Throwable t) { loadingDialog.dismiss(); Toast.makeText(getContext(), "Feil: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } private void showChildFormDialog(GravityForm childForm, String parentFieldId) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); childInputViews.clear(); childRequiredFieldsMap.clear(); childFileUploads.clear(); ScrollView scrollView = new ScrollView(getContext()); LinearLayout layout = new LinearLayout(getContext()); layout.setOrientation(LinearLayout.VERTICAL); layout.setPadding(30, 30, 30, 30); scrollView.addView(layout); for (GravityField field : childForm.fields) { if ("hidden".equals(field.type) || field.isHidden) continue; LinearLayout wrapper = new LinearLayout(getContext()); wrapper.setOrientation(LinearLayout.VERTICAL); wrapper.setPadding(0, 10, 0, 20); TextView label = new TextView(getContext()); String lText = field.label; if (field.isRequired) lText += " *"; label.setText(lText); label.setTypeface(null, Typeface.BOLD); wrapper.addView(label); if ("fileupload".equals(field.type)) { renderFileUploadField(wrapper, field, childInputViews, childRequiredFieldsMap, true); } else if ("product".equals(field.type)) { EditText input = new EditText(getContext()); input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); input.setHint("Kr 0.00"); wrapper.addView(input); childInputViews.put(field.id, input); childRequiredFieldsMap.put(field.id, field.isRequired); } else { renderTextField(wrapper, field, childInputViews, childRequiredFieldsMap); } layout.addView(wrapper); } builder.setView(scrollView); builder.setPositiveButton("Legg til vedlegg", null); builder.setNegativeButton("Avbryt", (d, w) -> d.dismiss()); AlertDialog dialog = builder.create(); dialog.show(); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { submitChildForm(childForm.id, dialog, parentFieldId); }); } private void submitChildForm(int childFormId, AlertDialog dialog, String parentFieldId) { JSONObject inputValues = new JSONObject(); for (Map.Entry entry : childInputViews.entrySet()) { String val = getInputValueGeneric(entry.getValue()); if (!val.isEmpty()) { try { inputValues.put("input_" + entry.getKey(), val); } catch (JSONException e) { e.printStackTrace(); } } } if (!childFileUploads.isEmpty()) { List fileParts = new ArrayList<>(); Map textParts = new HashMap<>(); try { JSONArray names = inputValues.names(); if (names != null) { for (int i = 0; i < names.length(); i++) { String key = names.getString(i); String val = inputValues.getString(key); textParts.put(key, RequestBody.create(MultipartBody.FORM, val)); } } 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(); Uri uri = fileEntry.getValue(); if (uri != null) { MultipartBody.Part part = getFilePart("input_" + fieldId, uri); if (part != null) fileParts.add(part); } } Toast.makeText(getContext(), "Laster opp vedlegg...", Toast.LENGTH_SHORT).show(); RetrofitClient.getApiService().submitMultipartForm(childFormId, textParts, fileParts).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(); if (json.has("is_valid") && json.get("is_valid").getAsBoolean()) { String entryId = json.has("entry_id") ? json.get("entry_id").getAsString() : ""; String desc = getInputValueGeneric(childInputViews.get("3")); String price = getInputValueGeneric(childInputViews.get("4")); addNestedEntry(entryId, desc, price); dialog.dismiss(); } else { Toast.makeText(getContext(), "Ugyldig respons fra server", Toast.LENGTH_SHORT).show(); } } catch (Exception e) { e.printStackTrace(); Toast.makeText(getContext(), "Feil ved parsing av svar", Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(getContext(), "Feil ved opplasting", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(retrofit2.Call call, Throwable t) { Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { e.printStackTrace(); } } } private void addNestedEntry(String entryId, String description, String price) { nestedEntries.add(new NestedEntry(entryId, description, price)); refreshNestedList(); } private void refreshNestedList() { if (nestedEntriesContainer == null) return; nestedEntriesContainer.removeAllViews(); double total = 0; List ids = new ArrayList<>(); for (NestedEntry entry : nestedEntries) { ids.add(entry.id); String cleanPrice = entry.price.replaceAll("[^0-9,.]", "").replace(",", "."); try { if (!cleanPrice.isEmpty()) total += Double.parseDouble(cleanPrice); } catch (NumberFormatException e) { } LinearLayout row = new LinearLayout(getContext()); row.setOrientation(LinearLayout.HORIZONTAL); row.setPadding(10, 10, 10, 10); TextView txt = new TextView(getContext()); txt.setText(entry.description + " (" + entry.price + ")"); txt.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); row.addView(txt); nestedEntriesContainer.addView(row); } if (totalAmountView != null) { totalAmountView.setText("Totalt: Kr " + String.format("%.2f", total)); } View hiddenField = inputViews.get("25"); if (hiddenField instanceof EditText) { ((EditText)hiddenField).setText(TextUtils.join(",", ids)); } } // --- FELLES METODER (FILE UPLOAD M/ CAMERA STØTTE) --- private void renderFileUploadField(LinearLayout container, GravityField field, Map viewsMap, Map reqMap, boolean isChild) { LinearLayout fileLayout = new LinearLayout(getContext()); fileLayout.setOrientation(LinearLayout.HORIZONTAL); Button btnUpload = new Button(getContext()); btnUpload.setText("Velg fil / Ta bilde"); btnUpload.setOnClickListener(v -> { pendingFileFieldId = field.id; isSelectingForChild = isChild; expandFormModule(); showFileSourceDialog(); }); TextView txtFileName = new TextView(getContext()); txtFileName.setText("Ingen fil valgt"); txtFileName.setPadding(20, 0, 0, 0); txtFileName.setTextColor(Color.GRAY); btnUpload.setTag(txtFileName); fileLayout.addView(btnUpload); fileLayout.addView(txtFileName); container.addView(fileLayout); viewsMap.put(field.id, btnUpload); reqMap.put(field.id, field.isRequired); } private void showFileSourceDialog() { String[] options = {"Ta bilde", "Velg fil"}; new AlertDialog.Builder(getContext()) .setTitle("Last opp vedlegg") .setItems(options, (dialog, which) -> { if (which == 0) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { openCamera(); } else { requestPermissionLauncher.launch(Manifest.permission.CAMERA); } } else { if (filePickerLauncher != null) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); String[] mimeTypes = {"image/jpeg", "image/png", "application/pdf"}; intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); filePickerLauncher.launch(intent); } } }) .show(); } private void openCamera() { currentPhotoUri = createImageUri(); if (currentPhotoUri != null && takePictureLauncher != null) { try { takePictureLauncher.launch(currentPhotoUri); } catch (Exception e) { Toast.makeText(getContext(), "Kunne ikke starte kamera: " + e.getMessage(), Toast.LENGTH_SHORT).show(); Log.e(TAG, "Camera launch failed", e); } } else { Toast.makeText(getContext(), "Kunne ikke opprette bildefil", Toast.LENGTH_SHORT).show(); } } private Uri createImageUri() { try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES); File image = File.createTempFile(imageFileName, ".jpg", storageDir); return FileProvider.getUriForFile(requireContext(), "com.kbs.kbsintranett.fileprovider", image); } catch (IOException e) { e.printStackTrace(); return null; } } private void handleFileSelection(String fieldId, Uri uri, boolean isChild) { if (isChild) { childFileUploads.put(fieldId, uri); } else { fileUploads.put(fieldId, uri); } Map targetMap = isChild ? childInputViews : inputViews; View view = targetMap.get(fieldId); if (view instanceof Button) { TextView txtView = (TextView) view.getTag(); if (txtView != null) { txtView.setText(getFileName(uri)); txtView.setTextColor(Color.BLACK); } } } private String getFileName(Uri uri) { String result = null; if (uri.getScheme().equals("content")) { try (Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); if(index >= 0) result = cursor.getString(index); } } catch (Exception e) {} } if (result == null) { result = uri.getPath(); int cut = result.lastIndexOf('/'); if (cut != -1) result = result.substring(cut + 1); } return result; } private String getCleanTitle(String title) { if (title == null) return ""; Matcher m = TITLE_PATTERN.matcher(title.trim()); if (m.find()) { return m.group(2); } return title; } // --- STANDARD RENDER METODER --- private void renderTimeField(LinearLayout container, GravityField field, Map views, Map req) { EditText timeInput = new EditText(getContext()); timeInput.setFocusable(false); timeInput.setClickable(true); timeInput.setHint("00:00"); timeInput.setOnClickListener(v -> { expandFormModule(); Calendar mcurrentTime = Calendar.getInstance(); int hour = mcurrentTime.get(Calendar.HOUR_OF_DAY); int minute = mcurrentTime.get(Calendar.MINUTE); new TimePickerDialog(getContext(), (timePicker, selectedHour, selectedMinute) -> { timeInput.setText(String.format("%02d:%02d", selectedHour, selectedMinute)); evaluateAllConditionalLogic(); }, hour, minute, true).show(); }); container.addView(timeInput); views.put(field.id, timeInput); req.put(field.id, field.isRequired); } private void renderTextField(LinearLayout container, GravityField field, Map views, Map req) { EditText input = new EditText(getContext()); input.setPadding(30, 30, 30, 30); input.setBackgroundResource(android.R.drawable.edit_text); if ("number".equals(field.type) || "phone".equals(field.type)) { input.setInputType(InputType.TYPE_CLASS_PHONE); } else if ("email".equals(field.type)) { input.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); } else { input.setInputType(InputType.TYPE_CLASS_TEXT); } if (views == inputViews) { UserManager user = UserManager.getInstance(); String lowerLabel = field.label.toLowerCase(); if (lowerLabel.contains("e-post")) input.setText(user.getUserEmail()); if (lowerLabel.contains("navn") || lowerLabel.contains("melder")) input.setText(user.getUserDisplayName()); if (lowerLabel.contains("stilling")) input.setText(user.getStilling()); if (lowerLabel.contains("mobil")) input.setText(user.getMobiltelefon()); } input.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void onTextChanged(CharSequence s, int start, int before, int count) {} public void afterTextChanged(Editable s) { evaluateAllConditionalLogic(); } }); attachInteractionListener(input); container.addView(input); views.put(field.id, input); req.put(field.id, field.isRequired); } private void renderTextAreaField(LinearLayout container, GravityField field, Map views, Map req) { EditText input = new EditText(getContext()); input.setBackgroundResource(android.R.drawable.edit_text); input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); input.setMinLines(3); input.setGravity(android.view.Gravity.TOP | android.view.Gravity.START); attachInteractionListener(input); container.addView(input); views.put(field.id, input); req.put(field.id, field.isRequired); } private void renderRadioField(LinearLayout container, GravityField field, Map views, Map req) { RadioGroup group = new RadioGroup(getContext()); if (field.choices != null) { for (GravityField.Choice choice : field.choices) { RadioButton rb = new RadioButton(getContext()); rb.setText(choice.text); rb.setTag(choice.value); rb.setOnClickListener(v -> { expandFormModule(); evaluateAllConditionalLogic(); }); group.addView(rb); } } group.setOnCheckedChangeListener((g, i) -> { expandFormModule(); evaluateAllConditionalLogic(); }); container.addView(group); views.put(field.id, group); req.put(field.id, field.isRequired); } private void renderSelectField(LinearLayout container, GravityField field, Map views, Map req) { Spinner spinner = new Spinner(getContext()); spinner.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { expandFormModule(); } return false; }); List labels = new ArrayList<>(); labels.add("- Velg -"); if (field.choices != null) { for (GravityField.Choice c : field.choices) labels.add(c.text); } ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, labels); spinner.setAdapter(adapter); container.addView(spinner); views.put(field.id, spinner); req.put(field.id, field.isRequired); } private void renderConsentField(LinearLayout container, GravityField field, Map views, Map req) { CheckBox checkBox = new CheckBox(getContext()); String cbText = (field.checkboxLabel != null && !field.checkboxLabel.isEmpty()) ? field.checkboxLabel : field.label; checkBox.setText(cbText); String inputId = (field.inputs != null && !field.inputs.isEmpty()) ? field.inputs.get(0).id : field.id; checkBox.setTag("1"); checkBox.setOnCheckedChangeListener((b, c) -> { expandFormModule(); evaluateAllConditionalLogic(); }); container.addView(checkBox); views.put(inputId, checkBox); req.put(inputId, field.isRequired); } private void renderCheckboxField(LinearLayout container, GravityField field, Map views, Map req) { if (field.inputs != null) { for (int i = 0; i < field.inputs.size(); i++) { GravityField inputDef = field.inputs.get(i); CheckBox checkBox = new CheckBox(getContext()); checkBox.setText(inputDef.label); String value = "1"; if (field.choices != null && i < field.choices.size()) { value = field.choices.get(i).value; } checkBox.setTag(value); checkBox.setOnCheckedChangeListener((b, c) -> { expandFormModule(); evaluateAllConditionalLogic(); }); container.addView(checkBox); views.put(inputDef.id, checkBox); req.put(inputDef.id, false); } } } private void renderDateField(LinearLayout container, GravityField field, Map views, Map req) { EditText dateInput = new EditText(getContext()); if (field.readOnly || (formId == ID_REFUSJON_UTLEGG && "28".equals(field.id))) { SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); dateInput.setText(df.format(new Date())); dateInput.setFocusable(false); dateInput.setClickable(false); dateInput.setEnabled(false); dateInput.setTextColor(Color.BLACK); } else { dateInput.setFocusable(false); dateInput.setClickable(true); dateInput.setHint("dd.mm.yyyy"); dateInput.setOnClickListener(v -> { expandFormModule(); Calendar c = Calendar.getInstance(); new DatePickerDialog(getContext(), (view, year, month, dayOfMonth) -> { dateInput.setText(String.format("%02d.%02d.%d", dayOfMonth, month + 1, year)); evaluateAllConditionalLogic(); }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show(); }); } dateInput.setPadding(30, 30, 30, 30); dateInput.setBackgroundResource(android.R.drawable.edit_text); container.addView(dateInput); views.put(field.id, dateInput); req.put(field.id, field.isRequired); } private void renderCompositeField(LinearLayout container, GravityField parentField, Map views, Map req) { UserManager user = UserManager.getInstance(); boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER); List inputs = new ArrayList<>(parentField.inputs); if ("address".equals(parentField.type)) { Collections.sort(inputs, (f1, f2) -> Integer.compare(getAddressScore(f1.label), getAddressScore(f2.label))); } for (GravityField subField : inputs) { if (subField.isHidden || "hidden".equals(subField.visibility)) continue; TextView subLabel = new TextView(getContext()); String subLabelText = subField.label; boolean isSubRequired = parentField.isRequired; if ("address".equals(parentField.type) && subField.id.endsWith(".2")) { isSubRequired = false; } if (isSubRequired) subLabelText += " *"; subLabel.setText(subLabelText); subLabel.setTextColor(Color.GRAY); subLabel.setTextSize(12); subLabel.setPadding(0, 10, 0, 0); container.addView(subLabel); EditText subInput = new EditText(getContext()); subInput.setPadding(30, 30, 30, 30); subInput.setBackgroundResource(android.R.drawable.edit_text); subInput.setInputType(InputType.TYPE_CLASS_TEXT); if (isPersonalia && parentField.label.toLowerCase().contains("navn") && !parentField.label.toLowerCase().contains("pårørende")) { String lowerSub = subField.label.toLowerCase(); if (lowerSub.contains("fornavn")) subInput.setText(user.getFirstName()); else if (lowerSub.contains("etternavn")) subInput.setText(user.getLastName()); } attachInteractionListener(subInput); container.addView(subInput); views.put(subField.id, subInput); req.put(subField.id, isSubRequired); } } private void addSectionHeader(LinearLayout container, String title, String descText) { TextView sectionHeader = new TextView(getContext()); sectionHeader.setText(title); sectionHeader.setTextSize(18); sectionHeader.setTypeface(null, Typeface.BOLD); sectionHeader.setTextColor(Color.parseColor("#0069B3")); sectionHeader.setPadding(0, 20, 0, 5); container.addView(sectionHeader); if (descText != null && !descText.isEmpty()) { TextView desc = new TextView(getContext()); desc.setText(Html.fromHtml(descText, Html.FROM_HTML_MODE_COMPACT)); desc.setTextSize(12); desc.setTextColor(Color.GRAY); container.addView(desc); } View line = new View(getContext()); line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2)); line.setBackgroundColor(Color.LTGRAY); line.setPadding(0,0,0,20); container.addView(line); } private int getAddressScore(String label) { if (label == null) return 99; String l = label.toLowerCase(); if (l.contains("adresselinje 1")) return 1; if (l.contains("adresselinje 2")) return 2; if (l.contains("postnummer") || l.contains("zip")) return 3; if (l.contains("poststed") || l.contains("city")) return 4; if (l.contains("land") || l.contains("country")) return 5; return 99; } private void evaluateAllConditionalLogic() { if (currentForm == null || currentForm.fields == null) return; for (GravityField field : currentForm.fields) { if (field.conditionalLogic == null) { setViewVisibility(field.id, true); continue; } boolean isMatch = evaluateLogic(field.conditionalLogic); boolean show = "show".equalsIgnoreCase(field.conditionalLogic.actionType); boolean shouldBeVisible = (show && isMatch) || (!show && !isMatch); setViewVisibility(field.id, shouldBeVisible); } } private boolean evaluateLogic(GravityField.ConditionalLogic logic) { if (logic.rules == null || logic.rules.isEmpty()) return true; boolean isAll = "all".equalsIgnoreCase(logic.logicType); boolean aggregatedResult = isAll; for (GravityField.Rule rule : logic.rules) { String val = getInputValue(rule.fieldId); boolean ruleMatch = checkRule(val, rule.operator, rule.value); if (isAll) { aggregatedResult = aggregatedResult && ruleMatch; if (!aggregatedResult) break; } else { aggregatedResult = aggregatedResult || ruleMatch; if (aggregatedResult) break; } } return aggregatedResult; } private boolean checkRule(String actualValue, String operator, String targetValue) { if (actualValue == null) actualValue = ""; if (targetValue == null) targetValue = ""; switch (operator.toLowerCase()) { case "is": return actualValue.equalsIgnoreCase(targetValue); case "isnot": return !actualValue.equalsIgnoreCase(targetValue); case "contains": return actualValue.toLowerCase().contains(targetValue.toLowerCase()); case "starts_with": return actualValue.toLowerCase().startsWith(targetValue.toLowerCase()); case "ends_with": return actualValue.toLowerCase().endsWith(targetValue.toLowerCase()); default: return false; } } private String getInputValue(String fieldId) { View view = inputViews.get(fieldId); return getInputValueGeneric(view); } private String getInputValueGeneric(View view) { if (view == null) return ""; if (view instanceof EditText) return ((EditText) view).getText().toString(); if (view instanceof RadioGroup) { int id = ((RadioGroup) view).getCheckedRadioButtonId(); if (id != -1) { View rb = view.findViewById(id); if (rb != null && rb.getTag() != null) return rb.getTag().toString(); } } if (view instanceof Spinner) { if (((Spinner) view).getSelectedItemPosition() == 0) return ""; Object item = ((Spinner) view).getSelectedItem(); return item != null ? item.toString() : ""; } if (view instanceof CheckBox) { CheckBox cb = (CheckBox) view; if (cb.isChecked()) { return cb.getTag() != null ? cb.getTag().toString() : "1"; } return ""; } return ""; } private void setViewVisibility(String fieldId, boolean visible) { View wrapper = fieldWrappers.get(fieldId); if (wrapper != null) { wrapper.setVisibility(visible ? View.VISIBLE : View.GONE); } } // --- SUBMISSION --- private void submitDynamicForm() { JSONObject inputValues = new JSONObject(); boolean hasValues = false; Log.d(TAG, "submitDynamicForm: Starting validation..."); for (Map.Entry entry : inputViews.entrySet()) { String fieldId = entry.getKey(); View view = entry.getValue(); View wrapper = fieldWrappers.get(fieldId); if (wrapper == null) { if (!view.isShown()) continue; } else { if (wrapper.getVisibility() != View.VISIBLE) continue; } String val = getInputValueGeneric(view); Boolean req = requiredFieldsMap.get(fieldId); if (req != null && req && val.isEmpty() && !(view instanceof Button)) { Log.d(TAG, "Validation failed for field " + fieldId); if (view instanceof EditText) { ((EditText)view).setError("Må fylles ut"); view.requestFocus(); } else { Toast.makeText(getContext(), "Fyll ut alle felt", Toast.LENGTH_SHORT).show(); } return; } if (!val.isEmpty()) { try { GravityField fieldDef = getGravityFieldById(fieldId); if (fieldDef != null && "date".equals(fieldDef.type)) { val = formatDateForApi(val); } inputValues.put("input_" + fieldId, val); hasValues = true; } catch (JSONException e) {} } } if (!hasValues && fileUploads.isEmpty()) { Log.d(TAG, "Submit aborted: Form is empty"); Toast.makeText(getContext(), "Skjemaet er tomt", Toast.LENGTH_SHORT).show(); return; } updateStatus("Sender inn..."); String cookie = UserManager.getInstance().getCookie(); Log.d(TAG, "Preparing submission payload: " + inputValues.toString()); if (!fileUploads.isEmpty()) { Log.d(TAG, "Submitting as Multipart..."); sendMultipart(inputValues); } else { Log.d(TAG, "Submitting as JSON..."); RequestBody body = RequestBody.create(MediaType.parse("application/json"), inputValues.toString()); String url = BASE_URL_GF + "/forms/" + formId + "/submissions"; Request request = new Request.Builder().url(url).post(body).header("Cookie", cookie).build(); client.newCall(request).enqueue(new okhttp3.Callback() { public void onFailure(okhttp3.Call call, IOException e) { Log.e(TAG, "JSON submit failed", e); updateStatus("Feil: " + e.getMessage()); } public void onResponse(okhttp3.Call call, Response response) { Log.d(TAG, "JSON response code: " + response.code()); if (response.isSuccessful()) { if (getActivity() != null) getActivity().runOnUiThread(() -> { Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); fetchFormEntries(); updateStatus("OK"); clearInputs(); }); } else { try { String errBody = response.body() != null ? response.body().string() : "No body"; Log.e(TAG, "Server error body: " + errBody); updateStatus("Feil (" + response.code() + "): " + errBody); } catch(Exception e){} } } }); } } private String formatDateForApi(String dateStr) { if (dateStr == null || dateStr.isEmpty()) return ""; try { SimpleDateFormat displayFormat = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); Date date = displayFormat.parse(dateStr); SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); return apiFormat.format(date); } catch (Exception e) { return dateStr; } } private GravityField getGravityFieldById(String id) { if (currentForm == null || currentForm.fields == null) return null; for (GravityField f : currentForm.fields) { if (f.id.equals(id)) return f; if (f.inputs != null) { for (GravityField sub : f.inputs) { if (sub.id.equals(id)) return sub; } } } return null; } private void sendMultipart(JSONObject inputValues) { List fileParts = new ArrayList<>(); Map textParts = new HashMap<>(); try { JSONArray names = inputValues.names(); if (names != null) { for(int i=0; i entry : fileUploads.entrySet()) { MultipartBody.Part part = getFilePart("input_" + entry.getKey(), entry.getValue()); if (part != null) fileParts.add(part); } RetrofitClient.getApiService().submitMultipartForm(formId, textParts, fileParts).enqueue(new retrofit2.Callback() { public void onResponse(retrofit2.Call call, retrofit2.Response response) { if (response.isSuccessful()) { if (getActivity() != null) getActivity().runOnUiThread(() -> { Toast.makeText(getContext(), "Sendt!", Toast.LENGTH_LONG).show(); fetchFormEntries(); updateStatus("OK"); clearInputs(); }); } else { updateStatus("Feil: " + response.code()); } } public void onFailure(retrofit2.Call call, Throwable t) { updateStatus("Feil: " + t.getMessage()); } }); } catch (Exception e) {} } private MultipartBody.Part getFilePart(String partName, Uri uri) { try { InputStream inputStream = getContext().getContentResolver().openInputStream(uri); String fileName = getFileName(uri); RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("application/octet-stream"); } @Override public void writeTo(BufferedSink sink) throws IOException { try (Source source = Okio.source(inputStream)) { sink.writeAll(source); } } }; return MultipartBody.Part.createFormData(partName, fileName, requestBody); } catch (Exception e) { return null; } } private void fetchFormEntries() { UserManager user = UserManager.getInstance(); String cookie = user.getCookie(); int userId = user.getUserId(); if (cookie == null) return; String searchJson = "{\"field_filters\":[{\"key\":\"created_by\",\"value\":\"" + userId + "\"}]}"; String encodedSearch = ""; try { encodedSearch = URLEncoder.encode(searchJson, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String url = BASE_URL_GF + "/entries?form_ids=" + formId + "&search=" + encodedSearch; Request request = new Request.Builder().url(url).header("Cookie", cookie).build(); client.newCall(request).enqueue(new okhttp3.Callback() { @Override public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException e) { Log.e(TAG, "Kunne ikke hente historikk", e); } @Override public void onResponse(@NonNull okhttp3.Call call, @NonNull Response response) throws IOException { if (response.isSuccessful()) { String jsonStr = response.body().string(); try { JSONObject json = new JSONObject(jsonStr); if (json.has("entries")) { JSONArray entries = json.getJSONArray("entries"); if (getActivity() != null) { getActivity().runOnUiThread(() -> { showHistory(entries); if (formId == ID_ANSATTEOPPLYSNINGER && entries.length() > 0) { try { prefillFormFromHistory(entries.getJSONObject(0)); } catch (JSONException e) { e.printStackTrace(); } } }); } } } catch (JSONException e) { e.printStackTrace(); } } } }); } private void showHistory(JSONArray entries) { if (historyContainer == null) return; historyContainer.removeAllViews(); if (entries.length() == 0) { if (lblHistory != null) lblHistory.setVisibility(View.GONE); return; } else { if (lblHistory != null) lblHistory.setVisibility(View.VISIBLE); } try { 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"); 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); item.setOnClickListener(v -> showEntryDetails(entry)); View line = new View(getContext()); line.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); line.setBackgroundColor(Color.LTGRAY); historyContainer.addView(item); historyContainer.addView(line); } } catch (JSONException e) { e.printStackTrace(); } } private void showEntryDetails(JSONObject entry) { if (formId == ID_REFUSJON_UTLEGG) { Log.d(TAG, "Form 16 detected. Checking for child entries..."); String nestedIds = entry.optString("25"); if (!nestedIds.isEmpty()) { Log.d(TAG, "Nested IDs found: " + nestedIds); List ids = new ArrayList<>(); if (nestedIds.startsWith("[") && nestedIds.endsWith("]")) { try { JSONArray jsonArray = new JSONArray(nestedIds); for(int i=0; iInnsendt: ").append(date).append("

"); text.append("Innsendt: ").append(date).append("\n\n"); if (currentForm != null && currentForm.fields != null) { for (GravityField field : currentForm.fields) { if ("section".equals(field.type) || "html".equals(field.type) || "captcha".equals(field.type)) continue; if (formId == ID_REFUSJON_UTLEGG && "25".equals(field.id)) continue; String value = ""; if (field.inputs != null && !field.inputs.isEmpty()) { for (GravityField input : field.inputs) { String subVal = entry.optString(input.id); if (!subVal.isEmpty()) value += " " + subVal; } } else { value = entry.optString(String.valueOf(field.id)); } if (!value.trim().isEmpty()) { if ("fileupload".equals(field.type)) { String cleanUrl = extractUrl(value); if (cleanUrl.startsWith("http")) { html.append("").append(field.label).append(":
") .append("Åpne fil

"); text.append(field.label).append(":\n").append(cleanUrl).append("\n\n"); } else { html.append("").append(field.label).append(":
").append(value).append("

"); text.append(field.label).append(":\n").append(value).append("\n\n"); } } else { html.append("").append(field.label).append(":
").append(value).append("

"); text.append(field.label).append(":\n").append(value).append("\n\n"); } } } } } catch (Exception e) {} } private void fetchChildEntriesRecursive(List ids, int index, StringBuilder html, StringBuilder text, AlertDialog loader) { if (index >= ids.size()) { loader.dismiss(); showFinalDialog(html, text); return; } String entryId = ids.get(index); 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(); String desc = json.has("3") ? json.get("3").getAsString() : "Uten beskrivelse"; String price = json.has("4") ? json.get("4").getAsString() : ""; html.append("Vedlegg ").append(index + 1).append(":
"); text.append("Vedlegg ").append(index + 1).append(":\n"); html.append(desc).append(" (").append(price).append(")
"); text.append(desc).append(" (").append(price).append(")\n"); if (json.has("1")) { JsonElement fileEl = json.get("1"); if (fileEl.isJsonArray()) { JsonArray arr = fileEl.getAsJsonArray(); for (int i = 0; i < arr.size(); i++) { String url = arr.get(i).getAsString().replace("\\/", "/"); html.append("Åpne fil ").append(i+1).append("
"); text.append(url).append("\n"); } } else { String rawString = fileEl.getAsString(); if (rawString.startsWith("[") && rawString.endsWith("]")) { try { JSONArray arr = new JSONArray(rawString); for (int i = 0; i < arr.length(); i++) { String url = arr.getString(i).replace("\\/", "/"); html.append("Åpne fil ").append(i+1).append("
"); text.append(url).append("\n"); } } catch (JSONException ex) { String clean = extractUrl(rawString); html.append("Åpne fil
"); text.append(clean).append("\n"); } } else { String clean = extractUrl(rawString); if(clean.startsWith("http")) { html.append("Åpne fil
"); text.append(clean).append("\n"); } } } } html.append("
"); text.append("\n"); } catch (Exception e) { Log.e(TAG, "Error parsing child entry", e); } } fetchChildEntriesRecursive(ids, index + 1, html, text, loader); } @Override public void onFailure(retrofit2.Call call, Throwable t) { fetchChildEntriesRecursive(ids, index + 1, html, text, loader); } }); } private void showFinalDialog(StringBuilder htmlBuilder, StringBuilder textBuilder) { ScrollView scroll = new ScrollView(getContext()); TextView text = new TextView(getContext()); text.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); text.setText(Html.fromHtml(htmlBuilder.toString(), Html.FROM_HTML_MODE_COMPACT)); text.setPadding(40, 40, 40, 40); scroll.addView(text); new AlertDialog.Builder(getContext()) .setTitle("Detaljer") .setView(scroll) .setPositiveButton("Lukk", null) .setNeutralButton("Del", (d, w) -> shareEntryDetails(textBuilder.toString())) .create() .show(); } private String extractUrl(String rawValue) { if (rawValue == null) return ""; String clean = rawValue.replace("[", "") .replace("]", "") .replace("\"", "") .replace("\\/", "/"); return clean.trim(); } private void shareEntryDetails(String text) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, text); sendIntent.setType("text/plain"); Intent shareIntent = Intent.createChooser(sendIntent, "Del innsending via..."); startActivity(shareIntent); } private void prefillFormFromHistory(JSONObject latestEntry) { if (latestEntry == null) return; for (Map.Entry entry : inputViews.entrySet()) { String fieldId = entry.getKey(); View view = entry.getValue(); if (latestEntry.has(fieldId)) { String value = latestEntry.optString(fieldId); if (value == null || value.isEmpty()) continue; if (view instanceof EditText) { ((EditText) view).setText(value); } else if (view instanceof RadioGroup) { RadioGroup group = (RadioGroup) view; for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); if (child instanceof RadioButton) { Object tag = child.getTag(); if (tag != null && tag.toString().equalsIgnoreCase(value)) { ((RadioButton) child).setChecked(true); break; } } } } else if (view instanceof CheckBox) { if ("1".equals(value) || "true".equalsIgnoreCase(value) || ((CheckBox)view).getText().toString().equals(value)) { ((CheckBox) view).setChecked(true); } } } } updateStatus("Skjemaet er forhåndsutfylt fra din siste innsending."); evaluateAllConditionalLogic(); } private void clearInputs() { for (View view : inputViews.values()) { if (view instanceof EditText) { ((EditText) view).setText(""); } else if (view instanceof CheckBox) { ((CheckBox) view).setChecked(false); } else if (view instanceof RadioGroup) { ((RadioGroup) view).clearCheck(); } else if (view instanceof Button) { Object tag = view.getTag(); if (tag instanceof TextView) { ((TextView) tag).setText("Ingen fil valgt"); } } } fileUploads.clear(); nestedEntries.clear(); if (nestedEntriesContainer != null) nestedEntriesContainer.removeAllViews(); if (totalAmountView != null) totalAmountView.setText("Kr 0,00"); } private static class NestedEntry { String id; String description; String price; NestedEntry(String id, String d, String p) { this.id = id; this.description = d; this.price = p; } } private void updateStatus(String msg) { if (getActivity() != null && txtStatus != null) { getActivity().runOnUiThread(() -> txtStatus.setText(msg)); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormsListFragment.java ============================================================ package com.kbs.kbsintranett; import android.graphics.Color; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class FormsListFragment extends Fragment { private LinearLayout formsContainer; private ProgressBar progressBar; private TextView errorText; private SwipeRefreshLayout swipeRefreshLayout; private static final Pattern TITLE_NUMBER_PATTERN = Pattern.compile("^(\\d+)[.\\s-]+\\s*(.*)"); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_forms_list, container, false); formsContainer = view.findViewById(R.id.forms_container); swipeRefreshLayout = view.findViewById(R.id.swipe_refresh); // Opprett en ProgressBar manuelt for første gangs lasting progressBar = new ProgressBar(getContext()); LinearLayout.LayoutParams progressParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); progressParams.gravity = Gravity.CENTER; progressBar.setLayoutParams(progressParams); formsContainer.addView(progressBar); errorText = new TextView(getContext()); errorText.setTextColor(Color.RED); errorText.setVisibility(View.GONE); errorText.setPadding(20, 20, 20, 20); formsContainer.addView(errorText); // Sett opp listener for swipe swipeRefreshLayout.setOnRefreshListener(() -> { fetchFormsList(); }); // Hent data første gang fetchFormsList(); return view; } private void fetchFormsList() { // Skjul feilmelding før ny henting if (errorText != null) errorText.setVisibility(View.GONE); RetrofitClient.getApiService().getFormsList().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (!isAdded()) return; // Stopp lasting-indikatorer progressBar.setVisibility(View.GONE); swipeRefreshLayout.setRefreshing(false); if (response.isSuccessful() && response.body() != null) { List allForms = response.body(); List activeForms = new ArrayList<>(); for (GravityForm form : allForms) { if (form.getIsActive()) { // NYTT: Hardkodet filtrering av skjemaer som ikke skal vises i listen // ID 10 = HMS-bekreftelse (Skal ligge i Håndbok/separat flyt) // ID 18 = Refusjon-vedlegg (Er et underskjema som brukes av ID 16) if (form.id == 10 || form.id == 18) { continue; } activeForms.add(form); } } Collections.sort(activeForms, new Comparator() { @Override public int compare(GravityForm f1, GravityForm f2) { int num1 = extractNumber(f1.title); int num2 = extractNumber(f2.title); return Integer.compare(num1, num2); } }); populateList(activeForms); } else { String msg = "Kunne ikke hente skjemaer. Kode: " + response.code(); if (response.code() == 401 || response.code() == 403) { msg += "\n(Mangler tilgang. Prøv å logge ut og inn igjen.)"; } showError(msg); } } @Override public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; // Stopp lasting-indikatorer progressBar.setVisibility(View.GONE); swipeRefreshLayout.setRefreshing(false); showError("Nettverksfeil: " + t.getMessage()); } }); } private void populateList(List forms) { formsContainer.removeAllViews(); if (forms.isEmpty()) { showError("Ingen aktive skjemaer funnet."); return; } for (GravityForm form : forms) { int formId = form.id; String cleanTitle = cleanTitle(form.title); addFormButton(formsContainer, cleanTitle, formId); } } private void addFormButton(LinearLayout container, String title, int formId) { Button btn = new Button(getContext()); btn.setText(title); btn.setBackgroundColor(Color.parseColor("#0069B3")); btn.setTextColor(Color.WHITE); btn.setPadding(30, 30, 30, 30); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins(0, 0, 0, 20); btn.setLayoutParams(params); btn.setOnClickListener(v -> { Bundle bundle = new Bundle(); bundle.putInt("formId", formId); Navigation.findNavController(v).navigate(R.id.action_formsListFragment_to_formsDetailFragment, bundle); }); container.addView(btn); } private void showError(String message) { if (formsContainer == null) return; formsContainer.removeAllViews(); TextView tv = new TextView(getContext()); tv.setText(message); tv.setTextColor(Color.RED); tv.setTextSize(16); formsContainer.addView(tv); } private int extractNumber(String title) { if (title == null) return 9999; Matcher m = TITLE_NUMBER_PATTERN.matcher(title.trim()); if (m.find()) { try { return Integer.parseInt(m.group(1)); } catch (NumberFormatException e) { return 9999; } } return 9999; } private String cleanTitle(String title) { if (title == null) return ""; Matcher m = TITLE_NUMBER_PATTERN.matcher(title.trim()); if (m.find()) { return m.group(2); } return title; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\FormSubmission.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.Map; public class FormSubmission { // Gravity Forms krever at dataene ligger inni "input_values" @SerializedName("input_values") public Map inputValues; public FormSubmission(Map inputValues) { this.inputValues = inputValues; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GoogleCalendarModels.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; /** * Hjelpeklasser for å parse JSON direkte fra Google Calendar API v3. */ public class GoogleCalendarModels { public static class Response { @SerializedName("items") public List items; } public static class Item { @SerializedName("summary") public String summary; @SerializedName("description") public String description; @SerializedName("location") public String location; @SerializedName("start") public TimePoint start; @SerializedName("end") public TimePoint end; @SerializedName("reminders") public Reminders reminders; } public static class TimePoint { @SerializedName("dateTime") public String dateTime; // Format: 2025-12-15T10:00:00+01:00 @SerializedName("date") public String date; // Format: 2025-12-15 (for heldags) } public static class Reminders { @SerializedName("useDefault") public boolean useDefault; @SerializedName("overrides") public List overrides; } public static class Override { @SerializedName("method") public String method; // f.eks "popup" @SerializedName("minutes") public int minutes; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; import java.util.Map; public class GravityEntryResponse { @SerializedName("total_count") public int totalCount; @SerializedName("entries") public List> entries; // Vi bruker Map fordi Gravity Forms returnerer alle feltverdier som nøkkel/verdi par i roten av objektet. // F.eks: { "id": "100", "form_id": "1", "1.3": "Ola", "1.6": "Nordmann" } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityField.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import java.util.List; public class GravityField { @SerializedName("id") public String id; @SerializedName("type") public String type; @SerializedName("inputType") public String inputType; @SerializedName("label") public String label; @SerializedName("adminLabel") public String adminLabel; @SerializedName("description") public String description; @SerializedName("defaultValue") public String defaultValue; @SerializedName("isRequired") public boolean isRequired; @SerializedName("checkboxLabel") public String checkboxLabel; @SerializedName("visibility") public String visibility; @JsonAdapter(ChoicesAdapter.class) @SerializedName("choices") public List choices; @SerializedName("content") public String content; // --- BRUKER ADAPTEREN HER --- @JsonAdapter(InputsAdapter.class) @SerializedName("inputs") public List inputs; // --------------------------- @SerializedName("isHidden") public boolean isHidden; @SerializedName("gwreadonly_enable") public boolean readOnly; @JsonAdapter(ConditionalLogicAdapter.class) @SerializedName("conditionalLogic") public ConditionalLogic conditionalLogic; @SerializedName("gppa-values-templates") public java.util.Map gppaTemplates; @SerializedName("gpnfForm") public String gpnfForm; public static class Choice { @SerializedName("text") public String text; @SerializedName("value") public String value; } public static class ConditionalLogic { @SerializedName("actionType") public String actionType; @SerializedName("logicType") public String logicType; @SerializedName("rules") public List rules; } public static class Rule { @SerializedName("fieldId") public String fieldId; @SerializedName("operator") public String operator; @SerializedName("value") public String value; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityForm.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; public class GravityForm { @SerializedName("id") public int id; @SerializedName("title") public String title; @SerializedName("description") public String description; // Endret til Object for å være robust mot både "1" (String) og 1 (Int) fra API @SerializedName("is_active") public Object isActive; @SerializedName("fields") public List fields; // Hjelpemetode for å sjekke om skjemaet er aktivt public boolean getIsActive() { if (isActive == null) return false; String s = isActive.toString(); return "1".equals(s) || "true".equalsIgnoreCase(s); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookAdapter.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookAdapter.java package com.kbs.kbsintranett; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class HandbookAdapter extends RecyclerView.Adapter { private List fullList; private List filteredList; private OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(HandbookItem item); } public HandbookAdapter(List items, OnItemClickListener listener) { this.fullList = items; this.filteredList = new ArrayList<>(items); this.listener = listener; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_handbook, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { HandbookItem item = filteredList.get(position); holder.title.setText(item.getTitle()); // Håndter ingress: Kutt etter 80 tegn hvis den er veldig lang String desc = item.getDescription(); if (desc == null) desc = ""; if (desc.length() > 100) { desc = desc.substring(0, 100) + "..."; } holder.desc.setText(desc); // Mapp ikon-type til ressurs (Utvidet liste fra PHP v3.0) int iconRes = R.drawable.ic_handbook_general; // Fallback String type = item.getIconType() != null ? item.getIconType() : ""; switch (type) { case "car": iconRes = R.drawable.ic_handbook_car; break; case "health": iconRes = R.drawable.ic_handbook_health; break; case "people": iconRes = R.drawable.ic_handbook_people; break; case "warning": iconRes = R.drawable.ic_handbook_warning; break; case "doc": iconRes = R.drawable.ic_handbook_doc; break; case "card": iconRes = R.drawable.ic_handbook_doc; break; // Bruker doc inntil videre case "computer": iconRes = R.drawable.ic_handbook_general; break; case "calendar": iconRes = R.drawable.ic_handbook_general; break; case "money": iconRes = R.drawable.ic_handbook_doc; break; case "helmet": iconRes = R.drawable.ic_handbook_warning; break; case "trash": iconRes = R.drawable.ic_handbook_general; break; case "book": iconRes = R.drawable.ic_book; break; // Gjenbruk eksisterende ic_book case "chat": iconRes = R.drawable.ic_handbook_people; break; default: iconRes = R.drawable.ic_handbook_general; break; } holder.icon.setImageResource(iconRes); holder.itemView.setOnClickListener(v -> listener.onItemClick(item)); } @Override public int getItemCount() { return filteredList.size(); } public void filter(String query) { filteredList.clear(); if (query.isEmpty()) { filteredList.addAll(fullList); } else { String q = query.toLowerCase(); for (HandbookItem item : fullList) { if (item.getTitle().toLowerCase().contains(q) || (item.getDescription() != null && item.getDescription().toLowerCase().contains(q))) { filteredList.add(item); } } } notifyDataSetChanged(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView title, desc; ImageView icon; public ViewHolder(View view) { super(view); title = view.findViewById(R.id.title); desc = view.findViewById(R.id.desc); icon = view.findViewById(R.id.icon); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookDetailFragment.java ============================================================ package com.kbs.kbsintranett; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import com.google.gson.JsonObject; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class HandbookDetailFragment extends Fragment { private int pageId; private String pageTitle; private WebView webView; private ProgressBar progressBar; // --- CSS: DESIGN MED MER LUFT --- private static final String CSS_STYLE = ""; // --- JAVASCRIPT: AUTOSCROLL --- private static final String JS_SCRIPT = ""; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_handbook_detail, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (getArguments() != null) { pageId = getArguments().getInt("page_id"); pageTitle = getArguments().getString("page_title"); } Toolbar toolbar = view.findViewById(R.id.detail_toolbar); toolbar.setTitle(pageTitle != null ? pageTitle : "Håndbok"); toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).navigateUp()); webView = view.findViewById(R.id.detail_webview); progressBar = view.findViewById(R.id.detail_loading); setupWebView(); fetchContent(); } private void setupWebView() { WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setCacheMode(WebSettings.LOAD_NO_CACHE); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return handleLinkClick(request.getUrl().toString()); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return handleLinkClick(url); } }); } private boolean handleLinkClick(String url) { // Ignorer klikk på accordion-lenker if (url.endsWith("#")) return true; String lowerUrl = url.toLowerCase(); int formIdToOpen = 0; // --- SPESIALHÅNDTERING: Link til Skjemaer basert på URL-nøkkelord --- // ID 1: Ansatteopplysninger if (lowerUrl.contains("ansatteopplysninger")) { formIdToOpen = 1; } // ID 2: Vernerunde else if (lowerUrl.contains("vernerunde")) { formIdToOpen = 2; } // ID 4: RUH (Rapport om uønsket hendelse) else if (lowerUrl.contains("uonsket-hendelse") || lowerUrl.contains("/ruh")) { formIdToOpen = 4; } // ID 5: Lån av verktøy/henger else if (lowerUrl.contains("lan-av") || lowerUrl.contains("verktoy")) { formIdToOpen = 5; } // ID 6: Avviksmelding else if (lowerUrl.contains("avviksmelding") || lowerUrl.contains("/avvik")) { formIdToOpen = 6; } // ID 9: Sikkerhetskurs / Kompetansebevis else if (lowerUrl.contains("sikkerhetskurs") || lowerUrl.contains("kompetansebevis")) { formIdToOpen = 9; } // ID 10: HMS Bekreftelse else if (lowerUrl.contains("hms-bekreftelse") || lowerUrl.contains("hms-policy")) { formIdToOpen = 10; } // ID 11: Egenmelding else if (lowerUrl.contains("egenmelding")) { formIdToOpen = 11; } // ID 12: Sjekkliste firmabil else if (lowerUrl.contains("sjekkliste") && (lowerUrl.contains("bil") || lowerUrl.contains("kjoretoy"))) { formIdToOpen = 12; } // ID 14: SJA (Sikker Jobbanalyse) else if (lowerUrl.contains("sja") || lowerUrl.contains("jobbanalyse")) { formIdToOpen = 14; } // ID 15: Fraværsvarsel else if (lowerUrl.contains("fravaersvarsel") || lowerUrl.contains("fravarsvarsel")) { formIdToOpen = 15; } // ID 16: Refusjon utlegg else if (lowerUrl.contains("refusjon") || lowerUrl.contains("utlegg")) { formIdToOpen = 16; } // ID 21: Medarbeidersamtale else if (lowerUrl.contains("medarbeidersamtale")) { formIdToOpen = 21; } // ID 22: Medarbeiderundersøkelse else if (lowerUrl.contains("medarbeiderundersokelse")) { formIdToOpen = 22; } // Hvis vi fant et skjema, naviger dit internt if (formIdToOpen > 0) { Bundle bundle = new Bundle(); bundle.putInt("formId", formIdToOpen); Navigation.findNavController(getView()).navigate(R.id.action_handbook_to_form, bundle); return true; } // --- STANDARD INTERN NAVIGASJON --- if (url.contains("intranet.kbs.no") || url.startsWith("/")) { int targetId = extractIdFromUrl(url); if (targetId > 0) { navigateToPage(targetId, "Laster..."); return true; } progressBar.setVisibility(View.VISIBLE); RetrofitClient.getApiService().lookupPageId(url).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { int id = response.body().get("id").getAsInt(); if (id > 0) { navigateToPage(id, "Laster..."); } else { openExternal(url); } } else { openExternal(url); } } @Override public void onFailure(Call call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); openExternal(url); } }); return true; } else { // Ekstern lenke Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(browserIntent); return true; } } private void navigateToPage(int id, String title) { Bundle bundle = new Bundle(); bundle.putInt("page_id", id); bundle.putString("page_title", title); Navigation.findNavController(getView()).navigate(R.id.action_handbook_to_detail, bundle); } private void openExternal(String url) { Intent intent = new Intent(getContext(), WebViewActivity.class); intent.putExtra(WebViewActivity.EXTRA_URL, url); intent.putExtra(WebViewActivity.EXTRA_TITLE, "KBS Intranett"); startActivity(intent); } private int extractIdFromUrl(String url) { Pattern p = Pattern.compile("[?&](p|page_id|post)=([0-9]+)"); Matcher m = p.matcher(url); if (m.find()) { try { return Integer.parseInt(m.group(2)); } catch (NumberFormatException e) { return 0; } } return 0; } private void fetchContent() { progressBar.setVisibility(View.VISIBLE); RetrofitClient.getApiService().getHandbookPage(pageId).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { HandbookPage page = response.body(); if (getView() != null) { Toolbar toolbar = getView().findViewById(R.id.detail_toolbar); if (toolbar != null) toolbar.setTitle(page.title); } String htmlContent = "" + "" + CSS_STYLE + JS_SCRIPT + ""; htmlContent += "

" + page.title + "

"; if (page.content != null) { htmlContent += page.content; } else { htmlContent += "

Ingen innhold funnet.

"; } htmlContent += ""; webView.loadDataWithBaseURL("https://intranet.kbs.no", htmlContent, "text/html", "UTF-8", null); } else { Toast.makeText(getContext(), "Kunne ikke laste innhold.", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookFragment.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookFragment.java package com.kbs.kbsintranett; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; // Viktig import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class HandbookFragment extends Fragment { private RecyclerView recyclerView; private ProgressBar progressBar; private EditText searchField; private HandbookAdapter adapter; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_handbook, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); recyclerView = view.findViewById(R.id.recycler_handbook); progressBar = view.findViewById(R.id.progressBar); searchField = view.findViewById(R.id.search_field); recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); searchField.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { if (adapter != null) adapter.filter(s.toString()); } }); fetchHandbook(); } private void fetchHandbook() { RetrofitClient.getApiService().getHandbookItems().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { adapter = new HandbookAdapter(response.body(), item -> { // NYTT: Naviger til Native Detail Fragment Bundle bundle = new Bundle(); bundle.putInt("page_id", item.getId()); bundle.putString("page_title", item.getTitle()); Navigation.findNavController(getView()) .navigate(R.id.action_handbook_to_detail, bundle); }); recyclerView.setAdapter(adapter); } else { String msg = "Kunne ikke laste håndboken. Kode: " + response.code(); if (response.code() == 404) msg += "\n(Fant ikke foreldresiden 'interninstruks-hms')"; Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show(); } } @Override public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); } }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookItem.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookItem.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.io.Serializable; public class HandbookItem implements Serializable { @SerializedName("id") private int id; // NYTT @SerializedName("title") private String title; @SerializedName("desc") private String description; @SerializedName("icon_type") private String iconType; @SerializedName("url") private String url; public int getId() { return id; } public String getTitle() { return title; } public String getDescription() { return description; } public String getIconType() { return iconType; } public String getUrl() { return url; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookPage.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\HandbookPage.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; public class HandbookPage { @SerializedName("id") public int id; @SerializedName("title") public String title; @SerializedName("content") public String content; // Dette er HTML-strengen } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeAdapter.java ============================================================ package com.kbs.kbsintranett; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.Paint; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.cardview.widget.CardView; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; public class HomeAdapter extends RecyclerView.Adapter { public static final int TYPE_CREATE_BUTTON = 1; public static final int TYPE_SECTION_TITLE = 2; public static final int TYPE_CALENDAR_ITEM = 3; public static final int TYPE_NEWS_ITEM = 4; public static final int TYPE_TASK_ITEM = 5; public static final int TYPE_EMPTY_TASKS = 6; private final List items; private final OnHomeClickListener listener; public interface OnHomeClickListener { void onCreateEventClick(); void onViewAllCalendarClick(); void onViewAllNewsClick(); void onViewAllTasksClick(); void onCalendarItemClick(CalendarEvent event); void onNewsItemClick(WpPost post); void onTaskItemClick(TaskItem task); void onTaskStatusChanged(TaskItem task, boolean isDone); } public HomeAdapter(List items, OnHomeClickListener listener) { this.items = items; this.listener = listener; } @Override public int getItemViewType(int position) { Object item = items.get(position); if (item instanceof CreateButtonItem) return TYPE_CREATE_BUTTON; if (item instanceof SectionTitleItem) return TYPE_SECTION_TITLE; if (item instanceof CalendarEvent) return TYPE_CALENDAR_ITEM; if (item instanceof WpPost) return TYPE_NEWS_ITEM; if (item instanceof TaskItem) return TYPE_TASK_ITEM; if (item instanceof EmptyTasksItem) return TYPE_EMPTY_TASKS; return -1; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch (viewType) { case TYPE_CREATE_BUTTON: return new CreateButtonViewHolder(inflater.inflate(R.layout.item_home_create_btn, parent, false)); case TYPE_SECTION_TITLE: return new SectionTitleViewHolder(inflater.inflate(R.layout.item_home_section_title, parent, false)); case TYPE_CALENDAR_ITEM: return new CalendarViewHolder(inflater.inflate(R.layout.item_calendar, parent, false)); case TYPE_TASK_ITEM: return new TaskViewHolder(inflater.inflate(R.layout.item_task, parent, false)); case TYPE_EMPTY_TASKS: return new EmptyViewHolder(inflater.inflate(R.layout.item_home_empty_tasks, parent, false)); case TYPE_NEWS_ITEM: return new NewsViewHolder(inflater.inflate(R.layout.item_news, parent, false)); default: throw new IllegalArgumentException("Ugyldig viewType"); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { Object item = items.get(position); if (holder instanceof CreateButtonViewHolder) { ((CreateButtonViewHolder) holder).btnCreate.setOnClickListener(v -> listener.onCreateEventClick()); } else if (holder instanceof SectionTitleViewHolder) { SectionTitleItem section = (SectionTitleItem) item; SectionTitleViewHolder vh = (SectionTitleViewHolder) holder; vh.title.setText(section.title); vh.btnViewAll.setOnClickListener(v -> { if (section.type == SectionTitleItem.TYPE_CALENDAR) listener.onViewAllCalendarClick(); else if (section.type == SectionTitleItem.TYPE_TASKS) listener.onViewAllTasksClick(); else listener.onViewAllNewsClick(); }); } else if (holder instanceof CalendarViewHolder) { CalendarEvent event = (CalendarEvent) item; CalendarViewHolder vh = (CalendarViewHolder) holder; vh.day.setText(event.getDay()); vh.month.setText(event.getMonth()); vh.time.setText(event.getTime()); vh.title.setText(event.getTitle()); boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:"); try { int color = Color.parseColor(isPrivate ? "#673AB7" : event.getCalendarColor()); vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(color)); } catch (Exception e) { vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#0069B3"))); } vh.itemView.setOnClickListener(v -> listener.onCalendarItemClick(event)); } else if (holder instanceof TaskViewHolder) { TaskItem task = (TaskItem) item; TaskViewHolder vh = (TaskViewHolder) holder; vh.title.setText(task.getTitle()); if (task.getDueDate() > 0) { SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); vh.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); } else { vh.date.setText("Ingen frist"); } String myEmail = UserManager.getInstance().getUserEmail(); boolean myStatus = task.getParticipantStatus(myEmail); vh.checkBox.setChecked(myStatus); long now = System.currentTimeMillis(); boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !myStatus; if (isOverdue) { vh.cardView.setCardBackgroundColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); vh.date.setTextColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_logo_accent_red)); } else { vh.cardView.setCardBackgroundColor(Color.WHITE); vh.date.setTextColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_muted_blue_gray)); } // --- REINTRODUSERT FREMDRIFTSBEREGNING --- int total = task.getAssigneeStatus().size(); int done = 0; for (Boolean isFinished : task.getAssigneeStatus().values()) { if (isFinished) done++; } vh.progress.setText("Fremdrift: " + done + "/" + total); // ------------------------------------------ vh.checkBox.setOnClickListener(v -> listener.onTaskStatusChanged(task, vh.checkBox.isChecked())); vh.itemView.setOnClickListener(v -> listener.onTaskItemClick(task)); } else if (holder instanceof NewsViewHolder) { WpPost post = (WpPost) item; NewsViewHolder vh = (NewsViewHolder) holder; vh.title.setText(post.getTitleStr()); vh.excerpt.setText(post.getExcerptStr()); vh.date.setText(post.date); String cat = post.getCategoryName(); vh.category.setText(cat); vh.category.setVisibility(cat.isEmpty() ? View.GONE : View.VISIBLE); String imgUrl = post.getFeaturedImageUrl(); if (imgUrl != null) { vh.image.setVisibility(View.VISIBLE); Glide.with(vh.image.getContext()).load(imgUrl).transition(DrawableTransitionOptions.withCrossFade()).centerCrop().into(vh.image); } else { vh.image.setVisibility(View.GONE); } vh.itemView.setOnClickListener(v -> listener.onNewsItemClick(post)); } } @Override public int getItemCount() { return items.size(); } // --- VIEW HOLDERS --- static class CreateButtonViewHolder extends RecyclerView.ViewHolder { Button btnCreate; CreateButtonViewHolder(View v) { super(v); btnCreate = v.findViewById(R.id.btn_create_event); } } static class SectionTitleViewHolder extends RecyclerView.ViewHolder { TextView title, btnViewAll; SectionTitleViewHolder(View v) { super(v); title = v.findViewById(R.id.txt_section_title); btnViewAll = v.findViewById(R.id.btn_view_all); } } static class CalendarViewHolder extends RecyclerView.ViewHolder { TextView day, month, title, time; LinearLayout dateBox; CalendarViewHolder(View view) { super(view); day = view.findViewById(R.id.cal_day); month = view.findViewById(R.id.cal_month); title = view.findViewById(R.id.cal_title); time = view.findViewById(R.id.cal_time); dateBox = view.findViewById(R.id.date_box_background); } } static class TaskViewHolder extends RecyclerView.ViewHolder { TextView title, date, progress; CheckBox checkBox; CardView cardView; TaskViewHolder(View v) { super(v); title = v.findViewById(R.id.task_title); date = v.findViewById(R.id.task_date); progress = v.findViewById(R.id.task_progress); // Husk denne checkBox = v.findViewById(R.id.task_checkbox); cardView = (CardView) v; } } static class NewsViewHolder extends RecyclerView.ViewHolder { TextView title, excerpt, date, category; ImageView image; NewsViewHolder(View v) { super(v); title = v.findViewById(R.id.news_title); excerpt = v.findViewById(R.id.news_excerpt); date = v.findViewById(R.id.news_date); category = v.findViewById(R.id.news_category); image = v.findViewById(R.id.news_image); } } static class EmptyViewHolder extends RecyclerView.ViewHolder { EmptyViewHolder(View v) { super(v); } } public static class CreateButtonItem {} public static class EmptyTasksItem {} public static class SectionTitleItem { public static final int TYPE_CALENDAR = 0; public static final int TYPE_NEWS = 1; public static final int TYPE_TASKS = 2; String title; int type; public SectionTitleItem(String t, int type) { this.title = t; this.type = type; } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java ============================================================ 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.ProgressBar; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; 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 java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import com.google.gson.JsonElement; public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickListener { private RecyclerView recyclerView; private HomeAdapter adapter; private ProgressBar mainProgressBar; private SwipeRefreshLayout swipeRefreshLayout; private List currentEvents = new ArrayList<>(); private List currentNews = new ArrayList<>(); private List currentTasks = new ArrayList<>(); private int activeNetworkCalls = 0; // Cache lever i 60 minutter. PUSH vil ugyldiggjøre den før tiden ved behov. private static final int CACHE_TTL_MINUTES = 60; private Handler timeoutHandler = new Handler(Looper.getMainLooper()); private Runnable timeoutRunnable = this::forceStopLoading; private ActivityResultLauncher requestPermissionLauncher; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> refreshData(true) ); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_home, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mainProgressBar = view.findViewById(R.id.main_loading_spinner); swipeRefreshLayout = view.findViewById(R.id.swipe_refresh_home); recyclerView = view.findViewById(R.id.main_recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); // Ved manuell swipe: Tving nettverksoppdatering swipeRefreshLayout.setOnRefreshListener(() -> refreshData(true)); // Ved oppstart: Bruk cache (false = ikke tving nettverk) refreshData(false); } @Override public void onDestroyView() { super.onDestroyView(); timeoutHandler.removeCallbacks(timeoutRunnable); } private void refreshData(boolean forceNetwork) { // 1. Last cache i bakgrunnen new Thread(() -> { if (getContext() == null) return; // Hent data fra disk List cachedApiEvents = CacheManager.getCachedCalendarEvents(getContext()); List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); for (CalendarEvent e : cachedApiEvents) CalendarManager.formatEventForUI(e); List mergedEvents = CalendarManager.mergeAndSort(cachedApiEvents, deviceEvents); List cachedNews = CacheManager.getCachedNewsPosts(getContext()); formatNewsDates(cachedNews); List cachedTasks = CacheManager.getTasks(getContext()); // 2. Oppdater UI på hovedtråden new Handler(Looper.getMainLooper()).post(() -> { if (!isAdded()) return; // Oppdater variablene currentEvents = mergedEvents; currentNews = cachedNews; currentTasks = cachedTasks; // Vis dataene vi fant umiddelbart (fjerner spinner hvis den var der) buildAndDisplayList(); // 3. Vurder om vi skal hente nytt fra nettet boolean cacheValidCal = CacheManager.isCacheValid(getContext(), "calendar", CACHE_TTL_MINUTES); boolean cacheValidNews = CacheManager.isCacheValid(getContext(), "news", CACHE_TTL_MINUTES); boolean cacheValidTasks = CacheManager.isCacheValid(getContext(), "tasks", CACHE_TTL_MINUTES); // Vi henter KUN hvis brukeren swiper (force) ELLER cachen er utgått på dato. // Vi henter IKKE bare fordi listene er tomme (da stoler vi på at cachen er korrekt tom). boolean needNetwork = forceNetwork || !cacheValidCal || !cacheValidNews || !cacheValidTasks; if (needNetwork) { // Vis spinner KUN hvis det er helt tomt fra før, eller ved manuell swipe boolean isEverythingEmpty = currentNews.isEmpty() && currentTasks.isEmpty() && currentEvents.isEmpty(); if (isEverythingEmpty && mainProgressBar != null) { mainProgressBar.setVisibility(View.VISIBLE); } else if (forceNetwork && swipeRefreshLayout != null) { swipeRefreshLayout.setRefreshing(true); } // Ellers: Silent update (ingen spinner, men vi henter i bakgrunnen) activeNetworkCalls = 3; timeoutHandler.removeCallbacks(timeoutRunnable); timeoutHandler.postDelayed(timeoutRunnable, 10000); // 10 sek timeout fetchCalendarData(); fetchNewsData(); fetchTaskData(); } else { // Cachen er god nok! Skjul evt spinnere. stopLoaders(); } }); }).start(); } private void stopLoaders() { if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE); if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); } private void forceStopLoading() { if (activeNetworkCalls > 0) { activeNetworkCalls = 0; stopLoaders(); buildAndDisplayList(); } } private void fetchCalendarData() { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); checkLoadingComplete(); return; } RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { List apiEvents = response.body(); CacheManager.saveCalendarEvents(getContext(), apiEvents); new Thread(() -> { List deviceEvents = CalendarManager.getDeviceEvents(getContext(), true); for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e); List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); new Handler(Looper.getMainLooper()).post(() -> { currentEvents = merged; checkLoadingComplete(); }); }).start(); } else { checkLoadingComplete(); } } @Override public void onFailure(Call> call, Throwable t) { checkLoadingComplete(); } }); } private void fetchNewsData() { RetrofitClient.getApiService().getPosts().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { currentNews = response.body(); CacheManager.saveNewsPosts(getContext(), currentNews); formatNewsDates(currentNews); } checkLoadingComplete(); } @Override public void onFailure(Call> call, Throwable t) { checkLoadingComplete(); } }); } private void fetchTaskData() { RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { currentTasks = response.body(); CacheManager.saveTasks(getContext(), currentTasks); } checkLoadingComplete(); } @Override public void onFailure(Call> call, Throwable t) { checkLoadingComplete(); } }); } private void formatNewsDates(List posts) { if (posts == null) return; SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); for (WpPost post : posts) { try { if (post.date != null && post.date.contains("T")) { Date date = rawFormat.parse(post.date); post.date = targetFormat.format(date); } } catch (Exception ignored) {} } } private synchronized void checkLoadingComplete() { activeNetworkCalls--; // Oppdater visningen fortløpende så snart data kommer inn buildAndDisplayList(); if (activeNetworkCalls <= 0) { activeNetworkCalls = 0; timeoutHandler.removeCallbacks(timeoutRunnable); stopLoaders(); } } private void buildAndDisplayList() { if (!isAdded()) return; List items = new ArrayList<>(); String myEmail = UserManager.getInstance().getUserEmail(); // 1. KALENDER items.add(new HomeAdapter.SectionTitleItem("Kommende hendelser", HomeAdapter.SectionTitleItem.TYPE_CALENDAR)); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); int calCount = 0; if (currentEvents != null) { for (CalendarEvent e : currentEvents) { if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { items.add(e); calCount++; } if (calCount >= 3) break; } } // 2. OPPGAVER items.add(new HomeAdapter.SectionTitleItem("Mine oppgaver", HomeAdapter.SectionTitleItem.TYPE_TASKS)); List myActiveTasks = new ArrayList<>(); if (currentTasks != null) { for (TaskItem t : currentTasks) { // Vis oppgaver jeg er deltaker i, som ikke er fullført av meg, og ikke helt lukket if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) { myActiveTasks.add(t); } } } if (myActiveTasks.isEmpty()) { items.add(new HomeAdapter.EmptyTasksItem()); } else { // Sortering: Dato (stigende), deretter de uten dato (alfabetisk) Collections.sort(myActiveTasks, (t1, t2) -> { boolean t1HasDate = t1.getDueDate() > 0; boolean t2HasDate = t2.getDueDate() > 0; 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()); }); for (int i = 0; i < Math.min(myActiveTasks.size(), 3); i++) { items.add(myActiveTasks.get(i)); } } // 3. NYHETER items.add(new HomeAdapter.SectionTitleItem("Siste nytt", HomeAdapter.SectionTitleItem.TYPE_NEWS)); if (currentNews != null) { items.addAll(currentNews); } adapter = new HomeAdapter(items, this); recyclerView.setAdapter(adapter); } // --- NAVIGASJON & KLIKK --- @Override public void onCreateEventClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_create_event); } @Override public void onViewAllCalendarClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_calendar_full); } @Override public void onViewAllNewsClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_news_full); } @Override public void onViewAllTasksClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_tasks); } @Override public void onCalendarItemClick(CalendarEvent event) { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); sheet.setOnEventChangeListener(() -> refreshData(true)); sheet.show(getParentFragmentManager(), "CalendarDetails"); } @Override public void onNewsItemClick(WpPost post) { Bundle bundle = new Bundle(); bundle.putSerializable("post_data", post); Navigation.findNavController(getView()).navigate(R.id.navigation_news_detail, bundle); } @Override public void onTaskItemClick(TaskItem task) { TaskDetailsBottomSheet sheet = new TaskDetailsBottomSheet(task, new TaskDetailsBottomSheet.OnTaskChangeListener() { @Override public void onTaskChanged() { saveAndSyncTasks(); } @Override public void onTaskDeleted(TaskItem taskToDelete) { currentTasks.remove(taskToDelete); saveAndSyncTasks(); } @Override public void onEditRequested(TaskItem taskToEdit) { AddTaskBottomSheet editDialog = new AddTaskBottomSheet(); editDialog.setTaskToEdit(taskToEdit); editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() { @Override public void onTaskAdded(TaskItem task) {} @Override public void onTaskUpdated(TaskItem task) { saveAndSyncTasks(); } }); editDialog.show(getChildFragmentManager(), "EditTask"); } }); sheet.show(getChildFragmentManager(), "TaskDetails"); } @Override public void onTaskStatusChanged(TaskItem task, boolean isDone) { String myEmail = UserManager.getInstance().getUserEmail(); task.setParticipantStatus(myEmail, isDone); if (task.getCreatedByEmail().equalsIgnoreCase(myEmail) && isDone) { task.setFullyCompleted(true); } saveAndSyncTasks(); } private void saveAndSyncTasks() { CacheManager.saveTasks(getContext(), currentTasks); buildAndDisplayList(); RetrofitClient.getApiService().syncTasks(currentTasks).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) {} @Override public void onFailure(Call call, Throwable t) {} }); } } ============================================================ 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 ============================================================ package com.kbs.kbsintranett; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; public class InputsAdapter implements JsonDeserializer> { @Override public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (json.isJsonNull()) { return new ArrayList<>(); } // Fikser krasjen: Hvis Gravity Forms sender "" i stedet for [], returner tom liste if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { return new ArrayList<>(); } if (json.isJsonArray()) { List list = new ArrayList<>(); for (JsonElement e : json.getAsJsonArray()) { list.add(context.deserialize(e, GravityField.class)); } return list; } return new ArrayList<>(); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\InternalLinkMovementMethod.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\InternalLinkMovementMethod.java package com.kbs.kbsintranett; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; // <-- Sjekk at denne er med! import android.text.Spannable; import android.text.method.LinkMovementMethod; import android.text.style.URLSpan; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; import android.widget.Toast; import androidx.navigation.Navigation; import com.google.gson.JsonObject; import java.util.regex.Matcher; import java.util.regex.Pattern; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class InternalLinkMovementMethod extends LinkMovementMethod { private static InternalLinkMovementMethod instance; private static final String TAG = "InternalLinkMethod"; public static InternalLinkMovementMethod getInstance() { if (instance == null) instance = new InternalLinkMovementMethod(); return instance; } @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); int line = widget.getLayout().getLineForVertical(y); int off = widget.getLayout().getOffsetForHorizontal(line, x); URLSpan[] link = buffer.getSpans(off, off, URLSpan.class); if (link.length != 0) { String url = link[0].getURL(); handleLink(widget.getContext(), url, widget); return true; } } return super.onTouchEvent(widget, buffer, event); } private void handleLink(Context context, String url, View view) { Log.d(TAG, "Link clicked: " + url); // 1. Sjekk om det er en intern lenke if (url.contains("intranet.kbs.no") || url.startsWith("/")) { // a) Prøv å finne ID hvis den finnes i URLen (?p=123) int pageId = extractIdFromUrl(url); if (pageId > 0) { navigateToInternalPage(view, pageId, "Laster..."); } else { // b) Det er en "pen" URL. Vi må spørre APIet hva IDen er. // Vi bruker Toast for å gi feedback om at noe skjer Toast.makeText(context, "Åpner side...", Toast.LENGTH_SHORT).show(); RetrofitClient.getApiService().lookupPageId(url).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null) { int id = response.body().get("id").getAsInt(); if (id > 0) { // Suksess! Naviger internt navigateToInternalPage(view, id, "Laster..."); } else { // Fant ikke ID, åpne i WebView som fallback openInWebView(context, url); } } else { openInWebView(context, url); } } @Override public void onFailure(Call call, Throwable t) { openInWebView(context, url); } }); } } else { // Ekstern lenke - åpne i nettleser Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); context.startActivity(browserIntent); } } private int extractIdFromUrl(String url) { Pattern p = Pattern.compile("[?&](p|page_id|post)=([0-9]+)"); Matcher m = p.matcher(url); if (m.find()) { try { return Integer.parseInt(m.group(2)); } catch (NumberFormatException e) { return 0; } } return 0; } private void navigateToInternalPage(View view, int pageId, String title) { try { Bundle bundle = new Bundle(); bundle.putInt("page_id", pageId); bundle.putString("page_title", title); Navigation.findNavController(view).navigate(R.id.action_handbook_to_detail, bundle); } catch (Exception e) { Log.e(TAG, "Kunne ikke navigere", e); } } private void openInWebView(Context context, String url) { Intent intent = new Intent(context, WebViewActivity.class); intent.putExtra(WebViewActivity.EXTRA_URL, url); intent.putExtra(WebViewActivity.EXTRA_TITLE, "KBS Intranett"); context.startActivity(intent); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\KbsApplication.java ============================================================ package com.kbs.kbsintranett; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; import android.os.Build; public class KbsApplication extends Application { public static final String CHANNEL_ID = "kbs_calendar_channel"; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } private void createNotificationChannel() { // Vi oppretter kanalen her ved oppstart, så den er klar uansett om // det er MainActivity eller en bakgrunnsjobb som trenger den. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( CHANNEL_ID, "KBS Kalendervarsler", NotificationManager.IMPORTANCE_HIGH ); channel.setDescription("Varsler for kalenderhendelser"); NotificationManager manager = getSystemService(NotificationManager.class); if (manager != null) { manager.createNotificationChannel(channel); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginFragment.java ============================================================ package com.kbs.kbsintranett; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; import androidx.navigation.Navigation; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.SignInButton; import com.google.android.gms.common.api.ApiException; import com.google.android.gms.tasks.Task; public class LoginFragment extends Fragment { private static final String TAG = "LoginFragment"; private GoogleSignInClient mGoogleSignInClient; private TextView statusText; private SignInButton signInButton; // Håndterer resultatet fra Google-vinduet private final ActivityResultLauncher signInLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { Task task = GoogleSignIn.getSignedInAccountFromIntent(result.getData()); handleGoogleResult(task); } ); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_login, container, false); statusText = view.findViewById(R.id.status_text); signInButton = view.findViewById(R.id.sign_in_button); signInButton.setSize(SignInButton.SIZE_WIDE); // Hent ID fra MainActivity String clientId = MainActivity.GOOGLE_WEB_CLIENT_ID; GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(clientId) .requestEmail() .build(); mGoogleSignInClient = GoogleSignIn.getClient(requireActivity(), gso); signInButton.setOnClickListener(v -> { statusText.setText("Starter Google innlogging..."); Intent signInIntent = mGoogleSignInClient.getSignInIntent(); signInLauncher.launch(signInIntent); }); return view; } private void handleGoogleResult(Task completedTask) { try { GoogleSignInAccount account = completedTask.getResult(ApiException.class); // 1. Google er OK. Nå logger vi inn på WordPress. statusText.setText("Google OK. Kobler til KBS Intranett..."); signInButton.setEnabled(false); // Hindre dobbeltklikk String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; AuthRepository.loginToWordPress( account.getIdToken(), account.getDisplayName(), account.getEmail(), photoUrl, new AuthRepository.AuthCallback() { @Override public void onSuccess(String role) { // 2. Alt er OK! Naviger til Hjem. if (isAdded()) { statusText.setText("Innlogging OK!"); NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); navController.navigate(R.id.action_login_to_home); } } @Override public void onError(String message) { if (isAdded()) { statusText.setText(message); signInButton.setEnabled(true); } } } ); } catch (ApiException e) { // --- KORRIGERT FEILMELDING --- Log.w(TAG, "signInResult:failed code=" + e.getStatusCode()); String message; if (e.getStatusCode() == 12500) { message = "Konto ikke funnet, eller konto uten rettigheter."; } else { message = "Google-feil: " + e.getStatusCode(); } statusText.setText(message); // --- SLUTT PÅ KORRIGERING --- } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginRequest.java ============================================================ package com.kbs.kbsintranett; public class LoginRequest { public String token; public LoginRequest(String token) { this.token = token; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginResponse.java package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.util.List; public class LoginResponse { public boolean success; @SerializedName("full_cookie") public String fullCookie; public String role; @SerializedName("user_id") public int userId; @SerializedName("first_name") public String firstName; @SerializedName("last_name") public String lastName; @SerializedName("stilling") public String stilling; @SerializedName("mobiltelefon") public String mobiltelefon; // NYTT FELT: Liste over kalendere brukeren kan skrive til @SerializedName("writeable_calendars") public List writeableCalendars; public String message; } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java ============================================================ package com.kbs.kbsintranett; import android.app.AlarmManager; import android.app.AlertDialog; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.util.Log; import android.view.View; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.material.navigation.NavigationView; import java.util.concurrent.TimeUnit; public class MainActivity extends AppCompatActivity { public static final String GOOGLE_WEB_CLIENT_ID = BuildConfig.WEB_CLIENT_ID; private NavController navController; private DrawerLayout drawerLayout; private AppBarConfiguration appBarConfiguration; private ActivityResultLauncher requestPermissionLauncher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); drawerLayout = findViewById(R.id.drawer_layout); NavigationView navigationView = findViewById(R.id.nav_view); NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); if (navHostFragment != null) { navController = navHostFragment.getNavController(); appBarConfiguration = new AppBarConfiguration.Builder( R.id.navigation_home, R.id.navigation_calendar_full, R.id.navigation_tasks, R.id.navigation_forms, R.id.navigation_news_full, R.id.navigation_handbook) .setOpenableLayout(drawerLayout) .build(); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); // Vi bruker en tilpasset listener for å sikre at Hjem og andre punkter alltid fungerer navigationView.setNavigationItemSelectedListener(item -> { boolean handled = NavigationUI.onNavDestinationSelected(item, navController); if (handled || item.getItemId() == R.id.navigation_home) { if (item.getItemId() == R.id.navigation_home) { navController.navigate(R.id.navigation_home); } drawerLayout.closeDrawer(GravityCompat.START); } return handled; }); navController.addOnDestinationChangedListener((controller, destination, arguments) -> { if (destination.getId() == R.id.navigation_login) { toolbar.setVisibility(View.GONE); drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } else { toolbar.setVisibility(View.VISIBLE); drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); updateNavHeader(navigationView); } }); } setupTaskReminders(); createNotificationChannel(); requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}); checkNotificationPermission(); checkExactAlarmPermission(); checkLoginState(); } private void updateNavHeader(NavigationView navigationView) { View headerView = navigationView.getHeaderView(0); TextView name = headerView.findViewById(R.id.nav_header_name); TextView email = headerView.findViewById(R.id.nav_header_email); UserManager user = UserManager.getInstance(); if (user.isLoggedIn()) { name.setText(user.getUserDisplayName()); email.setText(user.getUserEmail()); } } private void setupTaskReminders() { PeriodicWorkRequest taskCheck = new PeriodicWorkRequest.Builder( TaskReminderWorker.class, 2, TimeUnit.DAYS).build(); WorkManager.getInstance(this).enqueueUniquePeriodicWork( "TaskReminder", ExistingPeriodicWorkPolicy.KEEP, taskCheck); } @Override public boolean onSupportNavigateUp() { return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp(); } @Override public void onBackPressed() { if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } private void checkLoginState() { GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); if (account == null) navigateToLogin(); else refreshGoogleToken(); } private void refreshGoogleToken() { GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(GOOGLE_WEB_CLIENT_ID).requestEmail().build(); GoogleSignInClient client = GoogleSignIn.getClient(this, gso); client.silentSignIn().addOnSuccessListener(account -> { String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null; AuthRepository.loginToWordPress(account.getIdToken(), account.getDisplayName(), account.getEmail(), photoUrl, new AuthRepository.AuthCallback() { @Override public void onSuccess(String role) { if (navController.getCurrentDestination().getId() == R.id.navigation_login) { navController.navigate(R.id.action_login_to_home); } } @Override public void onError(String message) { navigateToLogin(); } }); }).addOnFailureListener(e -> navigateToLogin()); } private void navigateToLogin() { if (navController != null && navController.getCurrentDestination() != null && navController.getCurrentDestination().getId() != R.id.navigation_login) { navController.navigate(R.id.navigation_login); } } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", "Varsler", NotificationManager.IMPORTANCE_HIGH); getSystemService(NotificationManager.class).createNotificationChannel(channel); } } private void checkNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS); } } } private void checkExactAlarmPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\MyFirebaseMessagingService.java ============================================================ package com.kbs.kbsintranett; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class MyFirebaseMessagingService extends FirebaseMessagingService { private static final String TAG = "FCMService"; private static final String CHANNEL_ID = "kbs_calendar_channel"; @Override public void onNewToken(@NonNull String token) { super.onNewToken(token); AuthRepository.updateDeviceToken(token); } @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); // 1. DATA PAYLOAD (Bakgrunnsoppdatering) if (remoteMessage.getData().size() > 0) { String forceRefresh = remoteMessage.getData().get("force_refresh"); String type = remoteMessage.getData().get("type"); // "tasks_update", "news_update", "calendar_update" if ("true".equalsIgnoreCase(forceRefresh)) { Log.d(TAG, "Mottok force_refresh. Type: " + type); // Uansett hva det er, ugyldiggjør cachen slik at neste gang brukeren åpner appen, hentes ferske data. if ("news_update".equals(type)) { CacheManager.invalidateCache(getApplicationContext(), "news"); } else if ("tasks_update".equals(type)) { CacheManager.invalidateCache(getApplicationContext(), "tasks"); updateTasksInBackground(); // Hent oppgaver med en gang i bakgrunnen } else { // Kalender eller generelt CacheManager.invalidateCache(getApplicationContext(), "calendar"); updateCalendarAndAlarms(); } } } // 2. 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) { showNotification( remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody() ); } } private void updateCalendarAndAlarms() { RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { CacheManager.saveCalendarEvents(getApplicationContext(), response.body()); AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body()); } } @Override public void onFailure(Call> call, Throwable t) {} }); } private void updateTasksInBackground() { // Henter oppgaver stille i bakgrunnen slik at de er klare når brukeren åpner appen RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful() && response.body() != null) { CacheManager.saveTasks(getApplicationContext(), response.body()); } } @Override public void onFailure(Call> call, Throwable t) {} }); } private void showNotification(String title, String message) { createNotificationChannel(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; } } Intent tapIntent = new Intent(this, MainActivity.class); tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_kbs) .setColor(ContextCompat.getColor(this, R.color.kbs_logo_blue)) .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true); NotificationManagerCompat.from(this).notify((int) System.currentTimeMillis(), builder.build()); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( CHANNEL_ID, "KBS Varsler", NotificationManager.IMPORTANCE_HIGH ); getSystemService(NotificationManager.class).createNotificationChannel(channel); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsAdapter.java ============================================================ package com.kbs.kbsintranett; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import java.util.List; public class NewsAdapter extends RecyclerView.Adapter { private List posts; private OnItemClickListener listener; // NYTT // Interface for klikk public interface OnItemClickListener { void onItemClick(WpPost post); } // Oppdatert konstruktør public NewsAdapter(List posts, OnItemClickListener listener) { this.posts = posts; this.listener = listener; } // Overload for bakoverkompatibilitet (hvis du ikke sender listener) public NewsAdapter(List posts) { this.posts = posts; this.listener = null; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { WpPost post = posts.get(position); holder.title.setText(post.getTitleStr()); holder.excerpt.setText(post.getExcerptStr()); holder.date.setText(post.date); String cat = post.getCategoryName(); if (!cat.isEmpty()) { holder.category.setText(cat); holder.category.setVisibility(View.VISIBLE); } else { holder.category.setVisibility(View.GONE); } String imageUrl = post.getFeaturedImageUrl(); if (imageUrl != null && !imageUrl.isEmpty()) { holder.image.setVisibility(View.VISIBLE); Glide.with(holder.itemView.getContext()) .load(imageUrl) .transition(DrawableTransitionOptions.withCrossFade()) .centerCrop() .into(holder.image); } else { holder.image.setVisibility(View.GONE); } // NYTT: Håndter klikk holder.itemView.setOnClickListener(v -> { if (listener != null) { listener.onItemClick(post); } }); } @Override public int getItemCount() { return posts.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView title, excerpt, date, category; ImageView image; public ViewHolder(View view) { super(view); title = view.findViewById(R.id.news_title); excerpt = view.findViewById(R.id.news_excerpt); date = view.findViewById(R.id.news_date); category = view.findViewById(R.id.news_category); image = view.findViewById(R.id.news_image); } } // NYTT: Metode for å oppdatere listen etter filtrering public void updateList(List newPosts) { this.posts = newPosts; notifyDataSetChanged(); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsDetailFragment.java ============================================================ package com.kbs.kbsintranett; import android.content.Context; import android.os.Bundle; 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; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; 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) { return inflater.inflate(R.layout.fragment_news_detail, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (getArguments() != null) { WpPost post = (WpPost) getArguments().getSerializable("post_data"); if (post != null) { setupViews(view, post); } } } private void setupViews(View view, WpPost post) { Toolbar toolbar = view.findViewById(R.id.detail_toolbar); toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).navigateUp()); ImageView image = view.findViewById(R.id.detail_image); 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); 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); } else { image.setBackgroundColor(getResources().getColor(android.R.color.darker_gray)); } title.setText(post.getTitleStr()); category.setText(post.getCategoryName()); date.setText("Publisert: " + post.date); author.setText("Av: " + post.getAuthorName()); // 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"); }); } } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsFullFragment.java ============================================================ package com.kbs.kbsintranett; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class NewsFullFragment extends Fragment { private RecyclerView recyclerViewNews; private RecyclerView recyclerViewCategories; private ProgressBar progressBar; private NewsAdapter newsAdapter; private List allPosts = new ArrayList<>(); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_news_full, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); recyclerViewNews = view.findViewById(R.id.recycler_news_full); recyclerViewCategories = view.findViewById(R.id.recycler_categories); progressBar = view.findViewById(R.id.loading_news_full); // Setup Nyhetsliste recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext())); // Setup Kategorier (Horisontal) recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); setupCategories(); fetchAllNews(); } private void setupCategories() { List categories = Arrays.asList( "Alle", "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", "Ferieavvikling", "Fest og moro", "Generell drift", "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" ); CategoryAdapter catAdapter = new CategoryAdapter(categories, selectedCategory -> { filterNews(selectedCategory); }); recyclerViewCategories.setAdapter(catAdapter); } private void fetchAllNews() { progressBar.setVisibility(View.VISIBLE); RetrofitClient.getApiService().getAllPosts().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); if (response.isSuccessful() && response.body() != null) { allPosts = response.body(); formatDates(allPosts); newsAdapter = new NewsAdapter(new ArrayList<>(allPosts), post -> { Bundle bundle = new Bundle(); bundle.putSerializable("post_data", post); Navigation.findNavController(getView()).navigate(R.id.action_newsFull_to_newsDetail, bundle); }); recyclerViewNews.setAdapter(newsAdapter); } else { Toast.makeText(getContext(), "Klarte ikke laste nyheter", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call> call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); } }); } private void filterNews(String category) { if (newsAdapter == null) return; List filteredList = new ArrayList<>(); if (category.equals("Alle")) { filteredList.addAll(allPosts); } else { for (WpPost post : allPosts) { if (post.getCategoryName().equals(category)) { filteredList.add(post); } } } newsAdapter.updateList(filteredList); } private void formatDates(List posts) { SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); for (WpPost post : posts) { try { Date date = rawFormat.parse(post.date); post.date = targetFormat.format(date); } catch (Exception e) {} } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsItem.java ============================================================ package com.kbs.kbsintranett; public class NewsItem { private String title; private String excerpt; // Kort tekst/ingress private String author; public NewsItem(String title, String excerpt, String author) { this.title = title; this.excerpt = excerpt; this.author = author; } public String getTitle() { return title; } public String getExcerpt() { return excerpt; } public String getAuthor() { return author; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationHelper.java ============================================================ package com.kbs.kbsintranett; import android.Manifest; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; public class NotificationHelper { private static final String CHANNEL_ID = "kbs_calendar_channel"; /** * En enkel metode for å vise et standard KBS-varsel. */ public static void showNotification(Context context, String title, String message) { // Sjekk tillatelse for Android 13+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; } } // Intent for å åpne appen når man trykker på varselet Intent intent = new Intent(context, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_kbs) // Bruker samme ikon som kalender .setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue)) .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify((int) System.currentTimeMillis(), builder.build()); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\ProfileFragment.java ============================================================ package com.kbs.kbsintranett; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; import androidx.navigation.Navigation; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; public class ProfileFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_profile, container, false); 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); TextView versionText = view.findViewById(R.id.tv_version_info); // NYTT UserManager user = UserManager.getInstance(); nameText.setText(user.getUserDisplayName()); emailText.setText(user.getUserEmail()); roleText.setText("Rolle: " + user.getUserRole()); // 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()) .apply(RequestOptions.circleCropTransform()) .into(profileImage); } closeBtn.setOnClickListener(v -> { Navigation.findNavController(view).navigateUp(); }); updateInfoBtn.setOnClickListener(v -> { Bundle bundle = new Bundle(); bundle.putInt("formId", 1); Navigation.findNavController(view).navigate(R.id.action_profile_to_form, bundle); }); logoutBtn.setOnClickListener(v -> performLogout()); return view; } private void performLogout() { GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(MainActivity.GOOGLE_WEB_CLIENT_ID) .requestEmail() .build(); GoogleSignInClient client = GoogleSignIn.getClient(requireActivity(), gso); client.signOut().addOnCompleteListener(task -> { UserManager.getInstance().logout(); RetrofitClient.clearClient(); NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); navController.navigate(R.id.action_profile_to_login); Toast.makeText(getContext(), "Du er nå logget ut", Toast.LENGTH_SHORT).show(); }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\RegisterDeviceRequest.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; public class RegisterDeviceRequest { @SerializedName("fcm_token") public String fcmToken; @SerializedName("platform") public String platform; public RegisterDeviceRequest(String fcmToken) { this.fcmToken = fcmToken; this.platform = "android"; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\RetrofitClient.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import java.io.IOException; import java.util.List; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitClient { private static final String BASE_URL = "https://intranet.kbs.no/"; private static Retrofit retrofit = null; public static WordPressApiService getApiService() { if (retrofit == null) { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); if (BuildConfig.DEBUG) { // I debug-modus logger vi det mest nødvendige logging.setLevel(HttpLoggingInterceptor.Level.BASIC); } else { // I release er vi stille for ytelse og sikkerhet logging.setLevel(HttpLoggingInterceptor.Level.NONE); } OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder(); String dynamicCookie = UserManager.getInstance().getCookie(); if (dynamicCookie != null && !dynamicCookie.isEmpty()) { builder.header("Cookie", dynamicCookie); } return chain.proceed(builder.build()); } }) .build(); Gson gson = new GsonBuilder() .registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter()) .setLenient() .create(); retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); } return retrofit.create(WordPressApiService.class); } public static void clearClient() { retrofit = null; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskAdapter.java ============================================================ package com.kbs.kbsintranett; import android.graphics.Color; import android.graphics.Paint; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.cardview.widget.CardView; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; public class TaskAdapter extends RecyclerView.Adapter { private List tasks; private OnTaskClickListener listener; private String currentUserEmail; public interface OnTaskClickListener { void onTaskClick(TaskItem task); void onStatusChanged(TaskItem task, boolean isDone); } public TaskAdapter(List tasks, OnTaskClickListener listener) { this.tasks = tasks; this.listener = listener; this.currentUserEmail = UserManager.getInstance().getUserEmail(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_task, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { TaskItem task = tasks.get(position); holder.title.setText(task.getTitle()); if (task.getDueDate() > 0) { SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); holder.date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); } else { holder.date.setText("Ingen frist"); } if (task.getCreatedByEmail() != null && !task.getCreatedByEmail().equalsIgnoreCase(currentUserEmail)) { holder.creator.setText("Tildelt av: " + task.getCreatedByName()); holder.creator.setVisibility(View.VISIBLE); } else { holder.creator.setVisibility(View.GONE); } // SJEKK: Er jeg en deltaker? boolean isParticipant = task.isUserParticipant(currentUserEmail); if (isParticipant) { // Hvis jeg er deltaker: Vis checkbox og la meg endre MIN status holder.checkBox.setVisibility(View.VISIBLE); boolean myStatus = task.getParticipantStatus(currentUserEmail); holder.checkBox.setOnCheckedChangeListener(null); // Hindre trigging ved resirkulering holder.checkBox.setChecked(myStatus); holder.checkBox.setOnClickListener(v -> listener.onStatusChanged(task, holder.checkBox.isChecked())); } else { // Hvis jeg IKKE er deltaker (f.eks. Admin som ser på "Alle"): // Skjul checkboxen i listen. Admin må åpne detaljer for å endre andres status. holder.checkBox.setVisibility(View.INVISIBLE); holder.checkBox.setOnClickListener(null); } long now = System.currentTimeMillis(); // Sjekk overtid (kun hvis frist er satt) // Er jeg ikke deltaker, sjekker vi om hele oppgaven er ferdig boolean isDoneToCheck = isParticipant ? task.getParticipantStatus(currentUserEmail) : task.isFullyCompleted(); boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !isDoneToCheck; if (isOverdue) { holder.cardView.setCardBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_soft_light_pink_beige)); holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_logo_accent_red)); SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault()); holder.date.setText("FORFALT: " + sdf.format(new Date(task.getDueDate()))); } else { holder.cardView.setCardBackgroundColor(Color.WHITE); holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_muted_blue_gray)); } if (isDoneToCheck || task.isFullyCompleted()) { holder.title.setPaintFlags(holder.title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); holder.title.setTextColor(Color.GRAY); holder.cardView.setCardBackgroundColor(Color.parseColor("#F5F5F5")); } else if (!isOverdue) { holder.title.setPaintFlags(holder.title.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); holder.title.setTextColor(Color.BLACK); } int total = task.getAssigneeStatus().size(); int done = 0; for (Boolean b : task.getAssigneeStatus().values()) if (b) done++; holder.progress.setText("Fremdrift: " + done + "/" + total); holder.itemView.setOnClickListener(v -> listener.onTaskClick(task)); } @Override public int getItemCount() { return tasks.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title, date, creator, progress; CheckBox checkBox; CardView cardView; ViewHolder(View v) { super(v); title = v.findViewById(R.id.task_title); date = v.findViewById(R.id.task_date); creator = v.findViewById(R.id.task_creator); progress = v.findViewById(R.id.task_progress); checkBox = v.findViewById(R.id.task_checkbox); cardView = (CardView) v; } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskDetailsBottomSheet.java ============================================================ package com.kbs.kbsintranett; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.switchmaterial.SwitchMaterial; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Map; public class TaskDetailsBottomSheet extends BottomSheetDialogFragment { private TaskItem task; private OnTaskChangeListener listener; public interface OnTaskChangeListener { void onTaskChanged(); void onTaskDeleted(TaskItem task); void onEditRequested(TaskItem task); } public TaskDetailsBottomSheet(TaskItem task, OnTaskChangeListener listener) { this.task = task; this.listener = listener; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View v = inflater.inflate(R.layout.bottom_sheet_task_details, container, false); TextView title = v.findViewById(R.id.detail_task_title); TextView date = v.findViewById(R.id.detail_task_date); TextView desc = v.findViewById(R.id.detail_task_desc); LinearLayout participantsContainer = v.findViewById(R.id.container_participants_status); SwitchMaterial switchNotify = v.findViewById(R.id.switch_notifications); LinearLayout ownerActions = v.findViewById(R.id.layout_owner_actions); Button btnDelete = v.findViewById(R.id.btn_delete_task); Button btnEdit = v.findViewById(R.id.btn_edit_task); Button btnClose = v.findViewById(R.id.btn_close_details); // NYTT title.setText(task.getTitle()); if (task.getDueDate() > 0) { SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd. MMMM yyyy", Locale.getDefault()); date.setText("Frist: " + sdf.format(new Date(task.getDueDate()))); } else { date.setText("Ingen frist"); } if (task.getDescription() != null && !task.getDescription().isEmpty()) { desc.setText(task.getDescription()); desc.setVisibility(View.VISIBLE); } participantsContainer.removeAllViews(); UserManager userManager = UserManager.getInstance(); String myEmail = userManager.getUserEmail(); boolean canManageOthers = task.getCreatedByEmail().equalsIgnoreCase(myEmail) || userManager.isAdmin(); for (Map.Entry entry : task.getAssigneeStatus().entrySet()) { String email = entry.getKey(); boolean isDone = entry.getValue(); if (canManageOthers) { CheckBox cb = new CheckBox(getContext()); cb.setText(email + (isDone ? " (Fullført)" : "")); cb.setChecked(isDone); if (isDone) cb.setTextColor(getResources().getColor(android.R.color.darker_gray)); else cb.setTextColor(getResources().getColor(R.color.black)); cb.setOnCheckedChangeListener((buttonView, isChecked) -> { task.setParticipantStatus(email, isChecked); cb.setText(email + (isChecked ? " (Fullført)" : "")); if (isChecked) cb.setTextColor(getResources().getColor(android.R.color.darker_gray)); else cb.setTextColor(getResources().getColor(R.color.black)); if (listener != null) listener.onTaskChanged(); // Sjekk om alle er ferdige, i så fall lukk vinduet if (isChecked && areAllParticipantsFinished()) { dismiss(); } }); participantsContainer.addView(cb); } else { TextView t = new TextView(getContext()); String status = isDone ? "✅ Fullført" : "⏳ Pågår"; t.setText("• " + email + ": " + status); t.setPadding(0, 8, 0, 8); t.setTextSize(14); participantsContainer.addView(t); } } switchNotify.setChecked(task.isNotificationsEnabled()); switchNotify.setOnCheckedChangeListener((btn, isChecked) -> { task.setNotificationsEnabled(isChecked); if (listener != null) listener.onTaskChanged(); }); if (canManageOthers) { ownerActions.setVisibility(View.VISIBLE); btnDelete.setOnClickListener(view -> { if (listener != null) listener.onTaskDeleted(task); dismiss(); }); btnEdit.setOnClickListener(view -> { if (listener != null) listener.onEditRequested(task); dismiss(); }); } else { ownerActions.setVisibility(View.GONE); } // NYTT: Lukk-knapp funksjonalitet btnClose.setOnClickListener(view -> dismiss()); return v; } private boolean areAllParticipantsFinished() { for (Boolean isDone : task.getAssigneeStatus().values()) { if (!isDone) return false; } return true; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskItem.java ============================================================ package com.kbs.kbsintranett; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class TaskItem implements Serializable { private String id; private String title; private String description; private long dueDate; private String createdByEmail; private String createdByName; private Map assigneeStatus = new HashMap<>(); private boolean notificationsEnabled = true; private boolean isFullyCompleted = false; public TaskItem(String title, String description, long dueDate) { this.id = UUID.randomUUID().toString(); this.title = title; this.description = description; this.dueDate = dueDate; this.createdByEmail = UserManager.getInstance().getUserEmail(); this.createdByName = UserManager.getInstance().getUserDisplayName(); } // Getters public String getId() { return id; } public String getTitle() { return title; } public String getDescription() { return description; } public long getDueDate() { return dueDate; } public String getCreatedByEmail() { return createdByEmail; } public String getCreatedByName() { return createdByName; } public boolean isNotificationsEnabled() { return notificationsEnabled; } public boolean isFullyCompleted() { return isFullyCompleted; } // Setters (NYTT FOR REDIGERING) public void setTitle(String title) { this.title = title; } public void setDescription(String description) { this.description = description; } public void setDueDate(long dueDate) { this.dueDate = dueDate; } public void setNotificationsEnabled(boolean enabled) { this.notificationsEnabled = enabled; } public void setFullyCompleted(boolean fullyCompleted) { this.isFullyCompleted = fullyCompleted; } // Participant logikk public void addAssignee(String email) { assigneeStatus.put(email, false); } public void setParticipantStatus(String email, boolean done) { assigneeStatus.put(email, done); } public boolean getParticipantStatus(String email) { return assigneeStatus.getOrDefault(email, false); } public boolean isUserParticipant(String email) { return assigneeStatus.containsKey(email); } public Map getAssigneeStatus() { return assigneeStatus; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskReminderWorker.java ============================================================ package com.kbs.kbsintranett; import android.content.Context; import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; import java.util.List; public class TaskReminderWorker extends Worker { public TaskReminderWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } @NonNull @Override public Result doWork() { Context context = getApplicationContext(); List tasks = CacheManager.getTasks(context); String myEmail = UserManager.getInstance().getUserEmail(); long now = System.currentTimeMillis(); for (TaskItem task : tasks) { if (!task.isNotificationsEnabled() || task.isFullyCompleted() || task.getParticipantStatus(myEmail)) continue; long diff = task.getDueDate() - now; // Varsle hvis fristen er i dag (innenfor 24 timer) if (diff > 0 && diff < 86400000L) { NotificationHelper.showNotification(context, "Frist i dag!", "Oppgaven '" + task.getTitle() + "' forfaller snart."); } // Varsle hvis over frist (hver gang worker kjører, f.eks annenhver dag) else if (diff < 0) { NotificationHelper.showNotification(context, "Over frist!", "Oppgaven '" + task.getTitle() + "' er forsinket."); } } return Result.success(); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\TasksFragment.java ============================================================ package com.kbs.kbsintranett; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; import com.google.gson.JsonElement; import java.util.ArrayList; import java.util.Collections; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickListener { private RecyclerView recyclerView; private TaskAdapter adapter; private TabLayout tabLayout; private SwipeRefreshLayout swipeRefresh; private CheckBox cbShowCompleted; private List allTasks = new ArrayList<>(); private String myEmail; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_tasks, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); myEmail = UserManager.getInstance().getUserEmail(); tabLayout = view.findViewById(R.id.task_tabs); recyclerView = view.findViewById(R.id.recycler_tasks); swipeRefresh = view.findViewById(R.id.swipe_refresh_tasks); cbShowCompleted = view.findViewById(R.id.cb_show_completed); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); setupTabs(); FloatingActionButton fab = view.findViewById(R.id.fab_add_task); fab.setOnClickListener(v -> { AddTaskBottomSheet dialog = new AddTaskBottomSheet(); dialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() { @Override public void onTaskAdded(TaskItem task) { allTasks.add(0, task); saveAndSync(); } @Override public void onTaskUpdated(TaskItem task) { saveAndSync(); } }); dialog.show(getChildFragmentManager(), "AddTask"); }); tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { updateFilterUI(tab.getPosition()); filterAndDisplay(); } @Override public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) {} }); cbShowCompleted.setOnCheckedChangeListener((buttonView, isChecked) -> filterAndDisplay()); swipeRefresh.setOnRefreshListener(this::fetchTasksFromServer); allTasks = CacheManager.getTasks(getContext()); updateFilterUI(tabLayout.getSelectedTabPosition()); filterAndDisplay(); fetchTasksFromServer(); } private void setupTabs() { tabLayout.removeAllTabs(); tabLayout.addTab(tabLayout.newTab().setText("Mine")); tabLayout.addTab(tabLayout.newTab().setText("Fullførte")); tabLayout.addTab(tabLayout.newTab().setText("Tildelt andre")); if (UserManager.getInstance().isEditorOrAbove()) { tabLayout.addTab(tabLayout.newTab().setText("Alle")); } } private void updateFilterUI(int tabIndex) { if (tabIndex == 3) { cbShowCompleted.setVisibility(View.VISIBLE); } else { cbShowCompleted.setVisibility(View.GONE); } } private void fetchTasksFromServer() { swipeRefresh.setRefreshing(true); RetrofitClient.getApiService().getTasks().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { swipeRefresh.setRefreshing(false); if (response.isSuccessful() && response.body() != null) { allTasks = response.body(); CacheManager.saveTasks(getContext(), allTasks); filterAndDisplay(); } } @Override public void onFailure(Call> call, Throwable t) { swipeRefresh.setRefreshing(false); Toast.makeText(getContext(), "Kunne ikke synkronisere oppgaver", Toast.LENGTH_SHORT).show(); } }); } private void filterAndDisplay() { List filtered = new ArrayList<>(); int selectedTab = tabLayout.getSelectedTabPosition(); for (TaskItem t : allTasks) { boolean isParticipant = t.isUserParticipant(myEmail); boolean isCreator = t.getCreatedByEmail().equalsIgnoreCase(myEmail); boolean iHaveDoneIt = t.getParticipantStatus(myEmail); boolean hasOtherParticipants = false; for (String email : t.getAssigneeStatus().keySet()) { if (!email.equalsIgnoreCase(myEmail)) { hasOtherParticipants = true; break; } } // NYTT: Sjekk dynamisk om ALLE deltakerne har fullført oppgaven boolean allParticipantsFinished = true; if (t.getAssigneeStatus().isEmpty()) { allParticipantsFinished = false; } else { for (Boolean isDone : t.getAssigneeStatus().values()) { if (!isDone) { allParticipantsFinished = false; break; } } } // En oppgave er "helt ferdig" hvis flagget er satt ELLER alle har krysset av boolean effectivelyFinished = t.isFullyCompleted() || allParticipantsFinished; // Oppdater objektet slik at det lagres riktig neste gang (valgfritt men lurt) if (effectivelyFinished && !t.isFullyCompleted()) { t.setFullyCompleted(true); } switch (selectedTab) { case 0: // MINE if (isParticipant && !iHaveDoneIt && !t.isFullyCompleted()) filtered.add(t); break; case 1: // FULLFØRTE if (t.isFullyCompleted() || (isParticipant && iHaveDoneIt)) filtered.add(t); break; case 2: // TILDELT ANDRE if (isCreator && !t.isFullyCompleted() && hasOtherParticipants) filtered.add(t); break; case 3: // ALLE (Admin) if (cbShowCompleted.isChecked()) { // Vis alt filtered.add(t); } else { // Vis kun hvis IKKE ferdig if (!effectivelyFinished) filtered.add(t); } break; } } // SORTERING Collections.sort(filtered, (t1, t2) -> { boolean t1HasDate = t1.getDueDate() > 0; boolean t2HasDate = t2.getDueDate() > 0; if (selectedTab == 1) { if (t1HasDate && !t2HasDate) return -1; if (!t1HasDate && t2HasDate) return 1; if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle()); return Long.compare(t2.getDueDate(), t1.getDueDate()); } else { if (t1HasDate && !t2HasDate) return -1; if (!t1HasDate && t2HasDate) return 1; if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle()); return Long.compare(t1.getDueDate(), t2.getDueDate()); } }); adapter = new TaskAdapter(filtered, this); recyclerView.setAdapter(adapter); } @Override public void onTaskClick(TaskItem task) { TaskDetailsBottomSheet sheet = new TaskDetailsBottomSheet(task, new TaskDetailsBottomSheet.OnTaskChangeListener() { @Override public void onTaskChanged() { saveAndSync(); } @Override public void onTaskDeleted(TaskItem taskToDelete) { allTasks.remove(taskToDelete); saveAndSync(); } @Override public void onEditRequested(TaskItem taskToEdit) { AddTaskBottomSheet editDialog = new AddTaskBottomSheet(); editDialog.setTaskToEdit(taskToEdit); editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() { @Override public void onTaskAdded(TaskItem task) {} @Override public void onTaskUpdated(TaskItem task) { saveAndSync(); } }); editDialog.show(getChildFragmentManager(), "EditTask"); } }); sheet.show(getChildFragmentManager(), "TaskDetails"); } @Override public void onStatusChanged(TaskItem task, boolean isDone) { task.setParticipantStatus(myEmail, isDone); // Her trenger vi ikke logikk for "hasOthers" osv, fordi saveAndSync() // kaller filterAndDisplay() som nå regner ut status dynamisk. saveAndSync(); } private void saveAndSync() { CacheManager.saveTasks(getContext(), allTasks); filterAndDisplay(); // Oppdaterer visningen umiddelbart RetrofitClient.getApiService().syncTasks(allTasks).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) {} @Override public void onFailure(Call call, Throwable t) {} }); } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\User.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class User implements Serializable { @SerializedName("id") private int id; @SerializedName("name") private String name; @SerializedName("email") private String email; @SerializedName("roles") private List roles; // NYTT: Liste over roller // For bruk i UI (sjekkbokser) private boolean isSelected = false; public int getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public List getRoles() { return roles != null ? roles : new ArrayList<>(); } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } @Override public String toString() { return name; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\UserFilterHelper.java ============================================================ package com.kbs.kbsintranett; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class UserFilterHelper { private static final List EXCLUDED_IDS = Arrays.asList(50, 51); // felles@kbs.no og kbs@kbs.no private static final String REQUIRED_DOMAIN = "@kbs.no"; public static List getFilteredUsers(List allUsers) { if (allUsers == null) return new ArrayList<>(); UserManager me = UserManager.getInstance(); String myEmail = me.getUserEmail(); List sanitizedList = new ArrayList<>(); for (User u : allUsers) { String email = u.getEmail() != null ? u.getEmail().toLowerCase() : ""; if (!email.endsWith(REQUIRED_DOMAIN)) continue; if (EXCLUDED_IDS.contains(u.getId())) continue; if (u.getRoles() == null || u.getRoles().isEmpty()) continue; sanitizedList.add(u); } if (me.isEditorOrAbove()) { return sanitizedList; } List finalResult = new ArrayList<>(); List myRoles = getRolesForEmail(sanitizedList, myEmail); // NY ROLLE LAGT TIL I LISTEN: amuhmsmiljogruppa List deptRoles = Arrays.asList( "serviceavdelingen", "automasjonsavdelingen", "prosjektavdelingen", "administrasjonen", "kbs_alle", "amuhmsmiljogruppa" ); for (User u : sanitizedList) { if (u.getEmail().equalsIgnoreCase(myEmail)) { finalResult.add(u); continue; } boolean sharesDepartment = false; for (String role : u.getRoles()) { String r = role.toLowerCase(); if (deptRoles.contains(r) && myRoles.contains(r)) { sharesDepartment = true; break; } } if (sharesDepartment) { finalResult.add(u); } } return finalResult; } private static List getRolesForEmail(List users, String email) { List roles = new ArrayList<>(); for (User u : users) { if (u.getEmail().equalsIgnoreCase(email)) { if (u.getRoles() != null) { for (String r : u.getRoles()) roles.add(r.toLowerCase()); } break; } } return roles; } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java ============================================================ // FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java package com.kbs.kbsintranett; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; /** * UserManager fungerer som en global sesjon for appen. * Den holder på informasjon om innlogget bruker, rettigheter og autentiserings-cookie. */ public class UserManager { private static UserManager instance; // Google Data private String userDisplayName; private String userEmail; private String googleIdToken; private String photoUrl; // WordPress Data private int userId; private String userRole; private String currentCookie; // Extended Info private String firstName; private String lastName; private String stilling; private String mobiltelefon; // FCM Token (Push) private String fcmToken; // NYTT: private List writeableCalendars = new ArrayList<>(); private UserManager() {} public static synchronized UserManager getInstance() { if (instance == null) { instance = new UserManager(); } return instance; } public void setUserData(String name, String email, String token, @Nullable String photoUrl) { this.userDisplayName = name; this.userEmail = email; this.googleIdToken = token; this.photoUrl = photoUrl; } public void setExtendedUserInfo(String firstName, String lastName, String stilling, String mobiltelefon) { this.firstName = firstName; this.lastName = lastName; this.stilling = stilling; this.mobiltelefon = mobiltelefon; } public void setWriteableCalendars(List calendars) { this.writeableCalendars = calendars != null ? calendars : new ArrayList<>(); } public List getWriteableCalendars() { return writeableCalendars; } public void setCookie(String cookie) { this.currentCookie = cookie; } public void setUserRole(String role) { this.userRole = role; } public void setUserId(int id) { this.userId = id; } public String getUserDisplayName() { return userDisplayName != null ? userDisplayName : ""; } public String getUserEmail() { return userEmail != null ? userEmail : ""; } public String getGoogleIdToken() { return googleIdToken; } public String getPhotoUrl() { return photoUrl; } public String getCookie() { return currentCookie; } public String getUserRole() { return userRole != null ? userRole : "subscriber"; } public int getUserId() { return userId; } public String getFirstName() { return firstName != null ? firstName : ""; } public String getLastName() { return lastName != null ? lastName : ""; } public String getStilling() { return stilling != null ? stilling : ""; } public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; } public void setFcmToken(String token) { this.fcmToken = token; } public String getFcmToken() { return fcmToken; } public boolean isLoggedIn() { return userEmail != null && !userEmail.isEmpty(); } public boolean isAdmin() { return "administrator".equalsIgnoreCase(userRole); } public boolean isEditorOrAbove() { return userRole != null && (userRole.equalsIgnoreCase("administrator") || userRole.equalsIgnoreCase("editor")); } public void logout() { userDisplayName = null; userEmail = null; googleIdToken = null; photoUrl = null; userRole = null; currentCookie = null; userId = 0; firstName = null; lastName = null; stilling = null; mobiltelefon = null; writeableCalendars.clear(); // Vi sletter ikke fcmToken ved logout, da enheten fortsatt er den samme } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\WebViewActivity.java ============================================================ package com.kbs.kbsintranett; import android.annotation.SuppressLint; import android.os.Bundle; import android.webkit.CookieManager; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; public class WebViewActivity extends AppCompatActivity { public static final String EXTRA_URL = "extra_url"; public static final String EXTRA_TITLE = "extra_title"; @SuppressLint("SetJavaScriptEnabled") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_webview); String url = getIntent().getStringExtra(EXTRA_URL); String title = getIntent().getStringExtra(EXTRA_TITLE); Toolbar toolbar = findViewById(R.id.toolbar); toolbar.setTitle(title != null ? title : "Håndbok"); toolbar.setNavigationIcon(android.R.drawable.ic_menu_revert); toolbar.setNavigationOnClickListener(v -> finish()); WebView webView = findViewById(R.id.webview); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setBuiltInZoomControls(true); settings.setDisplayZoomControls(false); // --- MAGIEN SKJER HER: INJISER COOKIE --- String cookie = UserManager.getInstance().getCookie(); if (cookie != null && !cookie.isEmpty()) { CookieManager cookieManager = CookieManager.getInstance(); cookieManager.setAcceptCookie(true); // Vi antar at domenet er intranet.kbs.no basert på APIet cookieManager.setCookie("https://intranet.kbs.no", cookie); } // ---------------------------------------- webView.setWebViewClient(new WebViewClient()); // Åpne linker i samme WebView if (url != null) { webView.loadUrl(url); } } } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\WordPressApiService.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.List; import java.util.Map; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Multipart; import retrofit2.http.Part; import retrofit2.http.PartMap; import retrofit2.http.Query; public interface WordPressApiService { // --- NYHETER --- @GET("wp-json/wp/v2/posts?per_page=10&_embed") Call> getPosts(); @GET("wp-json/wp/v2/posts?per_page=50&_embed") Call> getAllPosts(); // --- AUTENTISERING & ENHET --- @POST("wp-json/kbs/v1/login") Call googleLogin(@Body LoginRequest request); @POST("wp-json/kbs/v1/device/register") Call registerDevice(@Body RegisterDeviceRequest request); @GET("wp-json/kbs/v1/users") Call> getUsersList(); // --- KALENDER --- @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); @POST("wp-json/kbs/v1/calendar/create") Call createCalendarEvent(@Body CreateEventRequest request); @POST("wp-json/kbs/v1/calendar/update") Call updateCalendarEvent(@Body CreateEventRequest request); @POST("wp-json/kbs/v1/calendar/delete") Call deleteCalendarEvent(@Body CreateEventRequest request); // --- SKJEMAER (GRAVITY FORMS) --- @GET("wp-json/kbs/v1/forms") Call> getFormsList(); @GET("wp-json/kbs/v1/forms/{id}") Call getForm(@Path("id") int formId); @Multipart @POST("wp-json/gf/v2/forms/{id}/submissions") Call submitMultipartForm( @Path("id") int formId, @PartMap Map textFields, @Part List files ); @GET("wp-json/gf/v2/entries") Call getEntries( @Query("form_ids") int formId, @Query("search") String searchJson, @Query("paging[page_size]") int pageSize ); @GET("wp-json/gf/v2/entries/{entry_id}") Call getSingleEntry(@Path("entry_id") String entryId); // --- HÅNDBOK --- @GET("wp-json/kbs/v1/handbook") Call> getHandbookItems(); @GET("wp-json/kbs/v1/handbook/{id}") Call getHandbookPage(@Path("id") int id); @GET("wp-json/kbs/v1/lookup-id") Call lookupPageId(@Query("url") String url); // --- OPPGAVER (SYNKRONISERING) --- @GET("wp-json/kbs/v1/tasks") Call> getTasks(); @POST("wp-json/kbs/v1/tasks/sync") Call syncTasks(@Body List tasks); } ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\WpPost.java ============================================================ package com.kbs.kbsintranett; import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.Arrays; import java.util.List; public class WpPost implements Serializable { @SerializedName("title") public Rendered title; @SerializedName("excerpt") public Rendered excerpt; @SerializedName("content") public Rendered content; @SerializedName("date") public String date; @SerializedName("_embedded") public Embedded embedded; public static class Rendered implements Serializable { @SerializedName("rendered") public String renderedString; } public static class Embedded implements Serializable { @SerializedName("wp:featuredmedia") public List mediaList; @SerializedName("wp:term") public List> termList; // NYTT: Forfatter-liste @SerializedName("author") public List authorList; } public static class Media implements Serializable { @SerializedName("source_url") public String sourceUrl; } public static class Term implements Serializable { @SerializedName("name") public String name; } // NYTT: Forfatter-klasse public static class Author implements Serializable { @SerializedName("name") public String name; } public String getTitleStr() { return title != null ? title.renderedString : "Uten tittel"; } public String getExcerptStr() { return excerpt != null ? android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString().trim() : ""; } public String getContentStr() { return content != null ? content.renderedString : ""; } public String getFeaturedImageUrl() { if (embedded != null && embedded.mediaList != null && !embedded.mediaList.isEmpty()) { return embedded.mediaList.get(0).sourceUrl; } return null; } // NYTT: Hent forfatternavn public String getAuthorName() { if (embedded != null && embedded.authorList != null && !embedded.authorList.isEmpty()) { return embedded.authorList.get(0).name; } return "Ukjent"; // Fallback } public String getCategoryName() { if (embedded != null && embedded.termList != null && !embedded.termList.isEmpty()) { List categories = embedded.termList.get(0); if (categories == null || categories.isEmpty()) return ""; List priorityCategories = Arrays.asList( "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", "Ferieavvikling", "Fest og moro", "Generell drift", "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" ); for (Term term : categories) { if (priorityCategories.contains(term.name)) return term.name; } for (Term term : categories) { if (term.name.contains("Alle ansatte")) return "Til info"; } return categories.get(0).name; } return ""; } } ============================================================ FILSTI: app\src\main\res\color\selector_day_text.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\bg_category_selected.xml ============================================================ ============================================================ 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 ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_form.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_car.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_doc.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_general.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_health.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_people.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_handbook_warning.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_home.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_launcher_background.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml ============================================================ ============================================================ FILSTI: app\src\main\res\drawable\selector_day_toggle.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\activity_main.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\activity_webview.xml ============================================================ ============================================================ FILSTI: app\src\main\res\layout\bottom_sheet_add_task.xml ============================================================