diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt new file mode 100644 index 0000000..90203e0 --- /dev/null +++ b/hele_prosjektet.txt @@ -0,0 +1,11189 @@ +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 +============================================================ + + + + + + + + + + + + + +