kbsintranett/hele_prosjektet.txt

9621 lines
373 KiB
Text

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
============================================================
plugins {
alias(libs.plugins.android.application)
// NY LINJE: Aktiver Google Services plugin her
id("com.google.gms.google-services")
}
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"
}
// 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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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
============================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".KbsApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KBSIntranett"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true"
android:theme="@style/Theme.KBSIntranett">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".WebViewActivity" />
<receiver
android:name=".AlarmReceiver"
android:enabled="true"
android:exported="false" />
<!-- NYTT: Registrering av Firebase Messaging Service -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Standard ikon for Firebase varsler -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_kbs" />
<!-- Standard farge for Firebase varsler (KBS Blå) -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/kbs_logo_blue" />
</application>
</manifest>
============================================================
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<CalendarEvent> 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<LoginResponse>() {
@Override
public void onResponse(Call<LoginResponse> call, Response<LoginResponse> 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<LoginResponse> 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<JsonElement>() {
@Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> 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<JsonElement> 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.util.Log;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class CacheManager {
private static final String FILE_CALENDAR = "cache_calendar.json";
private static final String FILE_NEWS = "cache_news.json";
private static final String FILE_HANDBOOK = "cache_handbook.json";
private static final String TAG = "CacheManager";
private static final Gson gson = new Gson();
// --- KALENDER ---
public static void saveCalendarEvents(Context context, List<CalendarEvent> events) {
saveList(context, FILE_CALENDAR, events);
}
public static List<CalendarEvent> getCachedCalendarEvents(Context context) {
Type type = new TypeToken<List<CalendarEvent>>() {}.getType();
List<CalendarEvent> list = loadList(context, FILE_CALENDAR, type);
return list != null ? list : new ArrayList<>();
}
// --- NYHETER ---
public static void saveNewsPosts(Context context, List<WpPost> posts) {
saveList(context, FILE_NEWS, posts);
}
public static List<WpPost> getCachedNewsPosts(Context context) {
Type type = new TypeToken<List<WpPost>>() {}.getType();
List<WpPost> list = loadList(context, FILE_NEWS, type);
return list != null ? list : new ArrayList<>();
}
// --- HÅNDBOK ---
public static void saveHandbookItems(Context context, List<HandbookItem> items) {
saveList(context, FILE_HANDBOOK, items);
}
public static List<HandbookItem> getCachedHandbookItems(Context context) {
Type type = new TypeToken<List<HandbookItem>>() {}.getType();
List<HandbookItem> list = loadList(context, FILE_HANDBOOK, type);
return list != null ? list : new ArrayList<>();
}
// --- GENERISKE HJELPEMETODER ---
private static <T> void saveList(Context context, String filename, List<T> list) {
if (context == null || list == null) return;
new Thread(() -> {
try {
String json = gson.toJson(list);
FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(json.getBytes());
fos.close();
Log.d(TAG, "Lagret cache til " + filename);
} catch (Exception e) {
Log.e(TAG, "Feil ved lagring av cache: " + filename, e);
}
}).start();
}
private static <T> List<T> 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<CalendarAdapter.ViewHolder> {
private List<CalendarEvent> events;
private final OnItemClickListener listener;
// Farge for individuelle/private hendelser (Deep Purple)
private static final String PRIVATE_EVENT_COLOR = "#673AB7";
public interface OnItemClickListener {
void onItemClick(CalendarEvent event);
}
public CalendarAdapter(List<CalendarEvent> events, OnItemClickListener listener) {
this.events = events;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CalendarEvent event = events.get(position);
holder.day.setText(event.getDay());
holder.month.setText(event.getMonth());
holder.time.setText(event.getTime());
holder.title.setText(event.getTitle());
// NYTT: Sjekk om hendelsen er "privat" (har deltakere)
boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:");
try {
int color;
if (isPrivate) {
// Bruk privat farge
color = Color.parseColor(PRIVATE_EVENT_COLOR);
} else {
// Bruk kalenderens standardfarge
color = Color.parseColor(event.getCalendarColor());
}
holder.dateBox.setBackgroundTintList(ColorStateList.valueOf(color));
} catch (Exception e) {
// Fallback til standard blå hvis fargekoden er ugyldig
holder.dateBox.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#0069B3")));
}
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(event);
}
});
}
@Override
public int getItemCount() {
return events.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView day, month, title, time;
LinearLayout dateBox;
public ViewHolder(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);
}
}
}
============================================================
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("<b>Synlig for:</b><br/>");
for (String email : emails) {
sb.append("• ").append(email.trim()).append("<br/>");
}
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<JsonElement>() {
@Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> 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<JsonElement> 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<Integer> 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<Integer> 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<Integer> 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.ImageView;
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 java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class CalendarFullFragment extends Fragment {
private RecyclerView recyclerView;
private ProgressBar progressBar;
private TextView emptyView;
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);
ImageView backBtn = view.findViewById(R.id.btn_back_calendar);
layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
}
@Override
public void onResume() {
super.onResume();
fetchAllEvents();
}
private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE);
new Thread(() -> {
// HER ER ENDRINGEN: isPreview = false (Hent alt)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), false);
new Handler(Looper.getMainLooper()).post(() -> fetchApiEvents(deviceEvents));
}).start();
}
private void fetchApiEvents(List<CalendarEvent> deviceEvents) {
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
apiEvents = response.body();
for (CalendarEvent e : apiEvents) {
CalendarManager.formatEventForUI(e);
}
}
List<CalendarEvent> allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
if (allEvents.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
CalendarAdapter adapter = new CalendarAdapter(allEvents, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.setOnEventChangeListener(CalendarFullFragment.this::fetchAllEvents);
sheet.show(getParentFragmentManager(), "CalendarDetails");
});
recyclerView.setAdapter(adapter);
scrollToToday(allEvents);
}
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
if (!deviceEvents.isEmpty()) {
CalendarAdapter adapter = new CalendarAdapter(deviceEvents, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.show(getParentFragmentManager(), "CalendarDetails");
});
recyclerView.setAdapter(adapter);
scrollToToday(deviceEvents);
} else {
emptyView.setText("Ingen hendelser funnet.");
emptyView.setVisibility(View.VISIBLE);
}
}
});
}
private void scrollToToday(List<CalendarEvent> events) {
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
int scrollIndex = 0;
for (int i = 0; i < events.size(); i++) {
String raw = events.get(i).getRawDate();
if (raw != null && raw.compareTo(today) >= 0) {
scrollIndex = i;
break;
}
}
if (scrollIndex > 0) {
layoutManager.scrollToPositionWithOffset(scrollIndex, 0);
}
}
}
============================================================
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<String> getKbsCalendarIds(Context context) {
List<String> 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<CalendarEvent> getDeviceEvents(Context context, boolean isPreview) {
List<CalendarEvent> deviceEvents = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
return deviceEvents;
}
List<String> 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<String> 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<CalendarEvent> mergeAndSort(List<CalendarEvent> apiEvents, List<CalendarEvent> deviceEvents) {
List<CalendarEvent> all = new ArrayList<>();
Set<String> 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<CategoryAdapter.ViewHolder> {
private List<String> categories;
private String selectedCategory = "Alle"; // Standardvalg
private OnCategoryClickListener listener;
public interface OnCategoryClickListener {
void onCategoryClick(String category);
}
public CategoryAdapter(List<String> 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<List<GravityField.Choice>> {
@Override
public List<GravityField.Choice> 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<GravityField.Choice> 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<GravityField.ConditionalLogic> {
@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.Collections;
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;
// Deltakere
private List<User> allUsers = new ArrayList<>();
private List<User> filteredUsers = new ArrayList<>();
// Arrangør-sporing (for å ikke overskrive opprinnelig arrangør ved redigering)
private String originalOrganizer = null;
// EDIT MODE
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);
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);
// Initialiser tid (neste hele 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 ER I REDIGERINGS-MODUS
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());
// Vis/Skjul deltaker-knapp
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) {}
});
spinnerCalendar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String calName = (String) parent.getItemAtPosition(position);
updateFilteredUsers(calName);
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
private void fetchUsers() {
RetrofitClient.getApiService().getUsersList().enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (!isAdded()) return;
if (response.isSuccessful() && response.body() != null) {
allUsers = response.body();
if (spinnerCalendar.getSelectedItem() != null) {
updateFilteredUsers(spinnerCalendar.getSelectedItem().toString());
}
if (eventToEdit != null) {
parseParticipantsFromDescription(eventToEdit.getDescription());
}
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {}
});
}
private void updateFilteredUsers(String calendarName) {
if (allUsers == null || allUsers.isEmpty()) return;
filteredUsers.clear();
String requiredRole = "";
if (calendarName.equals("Serviceavdelingen")) requiredRole = "serviceavdelingen";
else if (calendarName.equals("Automasjonsavdelingen")) requiredRole = "automasjonsavdelingen";
else if (calendarName.equals("Prosjektavdelingen")) requiredRole = "prosjektavdelingen";
else if (calendarName.equals("Administrasjonen")) requiredRole = "administrasjonen";
else if (calendarName.equals("Felles")) requiredRole = "kbs_alle";
for (User user : allUsers) {
List<String> rawRoles = user.getRoles();
List<String> roles = new ArrayList<>();
for (String r : rawRoles) roles.add(r.toLowerCase());
boolean hasAccess = false;
if (roles.contains("administrator")) {
hasAccess = true;
}
else if (!requiredRole.isEmpty() && roles.contains(requiredRole.toLowerCase())) {
hasAccess = true;
}
if (hasAccess) {
filteredUsers.add(user);
}
}
}
private void showUserSelectionDialog() {
if (filteredUsers.isEmpty()) {
Toast.makeText(getContext(), "Ingen personer med riktig tilgang 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 : allUsers) {
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 (int i = 0; i < allUsers.size(); i++) {
if (allUsers.get(i).getEmail().equalsIgnoreCase(email.trim())) {
allUsers.get(i).setSelected(true);
}
}
}
updateParticipantPreview();
} else {
rbAll.setChecked(true);
}
}
private void prefillForm(CalendarEvent event) {
etTitle.setText(event.getTitle());
// Hent ut opprinnelig arrangør hvis den finnes
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:.+", "") // Fjern også arrangør fra tekstfeltet
.trim();
etDesc.setText(cleanDesc);
etLocation.setText(event.getLocation());
ArrayAdapter<String> adapter = (ArrayAdapter<String>) 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);
Date d = simpleFormat.parse(start);
startCal.setTime(d);
if (event.getRawEndDate() != null) {
Date e = simpleFormat.parse(event.getRawEndDate());
Calendar c = Calendar.getInstance();
c.setTime(e);
c.add(Calendar.DAY_OF_MONTH, -1);
endCal.setTime(c.getTime());
} else {
endCal.setTime(d);
}
} 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<Integer> existingReminders = event.getReminders();
if (!existingReminders.isEmpty()) {
for (int i = 0; i < chipGroupReminders.getChildCount(); i++) {
((Chip) chipGroupReminders.getChildAt(i)).setChecked(false);
}
for (int min : existingReminders) {
checkChipByMinutes(min);
}
}
} catch (Exception e) {
Log.e("KBS_EDIT", "Feil ved prefill", e);
}
}
private void checkChipByMinutes(int minutes) {
for (int i = 0; i < chipGroupReminders.getChildCount(); i++) {
Chip chip = (Chip) chipGroupReminders.getChildAt(i);
if ((int)chip.getTag() == minutes) {
chip.setChecked(true);
}
}
}
private String getCalendarColor(String name) {
switch (name) {
case "Felles": return "#0069B3";
case "Administrasjonen": return "#607D8B";
case "Serviceavdelingen": return "#E65100";
case "Automasjonsavdelingen": return "#2E7D32";
case "Prosjektavdelingen": return "#7B1FA2";
default: return "#888888";
}
}
private void setupCalendarSpinner() {
List<String> calendars = UserManager.getInstance().getWriteableCalendars();
if (calendars.isEmpty()) calendars.add("Felles");
ArrayAdapter<String> adapter = new ArrayAdapter<String>(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);
String calName = getItem(position);
String colorHex = getCalendarColor(calName);
view.setBackgroundColor(Color.parseColor(colorHex));
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);
String calName = getItem(position);
String colorHex = getCalendarColor(calName);
view.setBackgroundColor(Color.WHITE);
view.setTextColor(Color.parseColor(colorHex));
view.setTypeface(null, Typeface.BOLD);
return view;
}
};
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCalendar.setAdapter(adapter);
}
private void setupReminderChips() {
addChip("Ved start", 0);
addChip("5 min", 5);
addChip("10 min", 10);
addChip("15 min", 15);
addChip("30 min", 30);
addChip("1 time", 60);
addChip("2 timer", 120);
addChip("1 dag", 1440);
addChip("2 dager", 2880);
addChip("1 uke", 10080);
if (eventToEdit == null) checkChipByMinutes(15);
}
private void addChip(String text, int minutes) {
Chip chip = new Chip(getContext());
chip.setText(text);
chip.setTag(minutes);
chip.setCheckable(true);
chip.setClickable(true);
chipGroupReminders.addView(chip);
}
private List<Integer> getSelectedReminders() {
List<Integer> selected = new ArrayList<>();
for (int i = 0; i < chipGroupReminders.getChildCount(); i++) {
Chip chip = (Chip) chipGroupReminders.getChildAt(i);
if (chip.isChecked()) {
selected.add((Integer) chip.getTag());
}
}
return selected;
}
private void updateRecurrenceSpinner() {
String dayName = new SimpleDateFormat("EEEE", new Locale("no")).format(startCal.getTime());
int dayOfMonth = startCal.get(Calendar.DAY_OF_MONTH);
String monthName = new SimpleDateFormat("MMMM", new Locale("no")).format(startCal.getTime());
int weekNo = (startCal.get(Calendar.DAY_OF_MONTH) - 1) / 7 + 1;
String nthDayString = "månedlig den " + weekNo + ". " + dayName + "en";
List<String> options = new ArrayList<>();
options.add("Ikke gjenta");
options.add("Daglig");
options.add("Ukentlig på " + dayName);
options.add("Månedlig den " + dayOfMonth + ".");
options.add(capitalize(nthDayString));
options.add("Årlig den " + dayOfMonth + ". " + monthName);
options.add("Hver ukedag (man-fre)");
options.add("Egendefinert...");
ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, options);
spinnerRecurrence.setAdapter(adapter);
}
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:
int weekNo = (startCal.get(Calendar.DAY_OF_MONTH) - 1) / 7 + 1;
String dayCode = getDayCode(startCal.get(Calendar.DAY_OF_WEEK));
selectedRRule = "RRULE:FREQ=MONTHLY;BYDAY=" + weekNo + dayCode;
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 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);
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);
EditText etCount = view.findViewById(R.id.et_count);
Calendar customEndCal = Calendar.getInstance();
customEndCal.add(Calendar.MONTH, 1);
SimpleDateFormat sdf = new SimpleDateFormat("d. MMM yyyy", Locale.getDefault());
btnEndDatePicker.setText(sdf.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(sdf.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<?> parent, View view, int position, long id) {
layoutWeekdays.setVisibility(position == 1 ? View.VISIBLE : View.GONE);
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
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:");
int freqPos = spinnerFreq.getSelectedItemPosition();
String freq = "";
switch (freqPos) {
case 0: freq = "DAILY"; break;
case 1: freq = "WEEKLY"; break;
case 2: freq = "MONTHLY"; break;
case 3: freq = "YEARLY"; break;
}
rrule.append("FREQ=").append(freq);
String interval = etInterval.getText().toString();
if (!interval.isEmpty() && !interval.equals("1")) {
rrule.append(";INTERVAL=").append(interval);
}
if (freq.equals("WEEKLY")) {
List<String> 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));
}
}
int checkedId = rgEnd.getCheckedRadioButtonId();
if (checkedId == R.id.rb_date) {
customEndCal.set(Calendar.HOUR_OF_DAY, 23);
customEndCal.set(Calendar.MINUTE, 59);
customEndCal.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
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 (checkedId == 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 dateFmt = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault());
SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm", Locale.getDefault());
btnStartDate.setText(dateFmt.format(startCal.getTime()));
btnEndDate.setText(dateFmt.format(endCal.getTime()));
btnStartTime.setText(timeFmt.format(startCal.getTime()));
btnEndTime.setText(timeFmt.format(endCal.getTime()));
String preview = dateFmt.format(startCal.getTime());
if (!isAllDay) preview += " " + timeFmt.format(startCal.getTime());
preview += " - ";
if (isSameDay(startCal, endCal)) {
if (isAllDay) preview += " (Samme dag)";
else preview += timeFmt.format(endCal.getTime());
} else {
preview += dateFmt.format(endCal.getTime());
if (!isAllDay) preview += " " + timeFmt.format(endCal.getTime());
}
txtPreview.setText(preview);
}
private boolean isSameDay(Calendar c1, Calendar c2) {
return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) &&
c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
}
private String capitalize(String s) {
if (s == null || s.isEmpty()) return s;
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
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 String getCalendarSlug() {
if (spinnerCalendar.getSelectedItem() != null) {
return spinnerCalendar.getSelectedItem().toString();
}
return "Felles";
}
private void submitEvent() {
String title = etTitle.getText().toString().trim();
if (title.isEmpty()) {
etTitle.setError("Mangler tittel");
return;
}
boolean isAllDay = switchAllDay.isChecked();
String format = isAllDay ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm";
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
String startTimeStr = sdf.format(startCal.getTime());
String endTimeStr = sdf.format(endCal.getTime());
String location = etLocation.getText().toString();
String description = etDesc.getText().toString();
List<Integer> reminders = getSelectedReminders();
if (rbSpecific.isChecked() && allUsers != null) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (User u : allUsers) {
if (u.isSelected()) {
if (!first) sb.append(",");
sb.append(u.getEmail());
first = false;
}
}
if (sb.length() > 0) {
description += "\n\n#deltakere:" + sb.toString();
}
}
// NYTT: Legg til arrangør
String organizerTag;
if (originalOrganizer != null) {
organizerTag = originalOrganizer; // Behold opprinnelig ved redigering
} else {
organizerTag = UserManager.getInstance().getUserDisplayName(); // Ny hendelse
}
description += "\n#arrangor:" + organizerTag;
CreateEventRequest req = new CreateEventRequest(
title, description, location, startTimeStr, endTimeStr,
getCalendarSlug(), reminders, isAllDay, selectedRRule
);
if (eventToEdit != null) {
req.id = eventToEdit.getId();
}
Toast.makeText(getContext(), eventToEdit != null ? "Oppdaterer..." : "Oppretter...", Toast.LENGTH_SHORT).show();
final Context appContext = requireContext().getApplicationContext();
Call<JsonElement> call;
if (eventToEdit != null) {
call = RetrofitClient.getApiService().updateCalendarEvent(req);
} else {
call = RetrofitClient.getApiService().createCalendarEvent(req);
}
call.enqueue(new Callback<JsonElement>() {
@Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {
if (response.isSuccessful()) {
Toast.makeText(getContext(), eventToEdit != null ? "Hendelse oppdatert!" : "Hendelse opprettet!", Toast.LENGTH_LONG).show();
fetchCalendarAndSchedule(appContext);
Navigation.findNavController(getView()).navigateUp();
} else {
Toast.makeText(getContext(), "Feil (" + response.code() + ")", Toast.LENGTH_LONG).show();
}
}
@Override
public void onFailure(Call<JsonElement> call, Throwable t) {
Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show();
}
});
}
private void fetchCalendarAndSchedule(Context context) {
new Thread(() -> {
try {
if (context == null) return;
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
if (response.isSuccessful() && response.body() != null) {
AlarmScheduler.scheduleAlarmsForEvents(context, response.body());
}
} catch (Exception e) {
Log.e("CreateEvent", "Kunne ikke oppdatere alarmer", e);
}
}).start();
}
}
============================================================
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<Integer> reminders; // Liste, ikke int
@SerializedName("is_all_day")
public boolean isAllDay;
@SerializedName("recurrence")
public String recurrence;
// Oppdatert konstruktør som tar imot List<Integer>
public CreateEventRequest(String title, String description, String location, String startTime, String endTime, String calendarType, List<Integer> 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<String, View> fieldWrappers = new HashMap<>();
private Map<String, View> inputViews = new HashMap<>();
private Map<String, Boolean> requiredFieldsMap = new HashMap<>();
private Map<String, Uri> fileUploads = new HashMap<>();
// --- NESTED FORM (BARN) STATE ---
private Map<String, View> childInputViews = new HashMap<>();
private Map<String, Boolean> childRequiredFieldsMap = new HashMap<>();
private Map<String, Uri> childFileUploads = new HashMap<>();
private List<NestedEntry> 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<Intent> filePickerLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
private ActivityResultLauncher<String> 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<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> 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<GravityForm> 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<GravityForm>() {
@Override
public void onResponse(retrofit2.Call<GravityForm> call, retrofit2.Response<GravityForm> 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<GravityForm> 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<String, View> 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<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> 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<String, Uri> 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<JsonElement>() {
@Override
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> 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<JsonElement> 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<String> 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<String, View> viewsMap, Map<String, Boolean> 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<String, View> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> req) {
Spinner spinner = new Spinner(getContext());
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
expandFormModule();
}
return false;
});
List<String> labels = new ArrayList<>();
labels.add("- Velg -");
if (field.choices != null) {
for (GravityField.Choice c : field.choices) labels.add(c.text);
}
ArrayAdapter<String> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> 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<String, View> views, Map<String, Boolean> req) {
UserManager user = UserManager.getInstance();
boolean isPersonalia = (formId == ID_ANSATTEOPPLYSNINGER);
List<GravityField> 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<String, View> 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<MultipartBody.Part> fileParts = new ArrayList<>();
Map<String, RequestBody> textParts = new HashMap<>();
try {
JSONArray names = inputValues.names();
if (names != null) {
for(int i=0; i<names.length(); i++) {
String k = names.getString(i);
textParts.put(k, RequestBody.create(MultipartBody.FORM, inputValues.getString(k)));
}
}
for (Map.Entry<String, Uri> 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<JsonElement>() {
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> 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<JsonElement> 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<String> ids = new ArrayList<>();
if (nestedIds.startsWith("[") && nestedIds.endsWith("]")) {
try {
JSONArray jsonArray = new JSONArray(nestedIds);
for(int i=0; i<jsonArray.length(); i++) {
ids.add(jsonArray.getString(i));
}
} catch(JSONException e) {
Log.e(TAG, "Failed to parse nested IDs as JSON array", e);
}
} else {
for (String id : nestedIds.split(",")) {
ids.add(id.trim());
}
}
if (!ids.isEmpty()) {
AlertDialog loadingDialog = new AlertDialog.Builder(getContext())
.setMessage("Henter vedleggsdetaljer...")
.setCancelable(false)
.show();
StringBuilder accumulatedHtml = new StringBuilder();
StringBuilder accumulatedText = new StringBuilder();
appendBasicInfo(entry, accumulatedHtml, accumulatedText);
fetchChildEntriesRecursive(ids, 0, accumulatedHtml, accumulatedText, loadingDialog);
return;
}
}
}
StringBuilder htmlBuilder = new StringBuilder();
StringBuilder textBuilder = new StringBuilder();
appendBasicInfo(entry, htmlBuilder, textBuilder);
showFinalDialog(htmlBuilder, textBuilder);
}
private void appendBasicInfo(JSONObject entry, StringBuilder html, StringBuilder text) {
try {
String date = entry.optString("date_created");
html.append("<b>Innsendt:</b> ").append(date).append("<br><br>");
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("<b>").append(field.label).append(":</b><br>")
.append("<a href=\"").append(cleanUrl).append("\">Åpne fil</a><br><br>");
text.append(field.label).append(":\n").append(cleanUrl).append("\n\n");
} else {
html.append("<b>").append(field.label).append(":</b><br>").append(value).append("<br><br>");
text.append(field.label).append(":\n").append(value).append("\n\n");
}
} else {
html.append("<b>").append(field.label).append(":</b><br>").append(value).append("<br><br>");
text.append(field.label).append(":\n").append(value).append("\n\n");
}
}
}
}
} catch (Exception e) {}
}
private void fetchChildEntriesRecursive(List<String> 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<JsonElement>() {
@Override
public void onResponse(retrofit2.Call<JsonElement> call, retrofit2.Response<JsonElement> 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("<b>Vedlegg ").append(index + 1).append(":</b><br>");
text.append("Vedlegg ").append(index + 1).append(":\n");
html.append(desc).append(" (").append(price).append(")<br>");
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("<a href=\"").append(url).append("\">Åpne fil ").append(i+1).append("</a><br>");
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("<a href=\"").append(url).append("\">Åpne fil ").append(i+1).append("</a><br>");
text.append(url).append("\n");
}
} catch (JSONException ex) {
String clean = extractUrl(rawString);
html.append("<a href=\"").append(clean).append("\">Åpne fil</a><br>");
text.append(clean).append("\n");
}
} else {
String clean = extractUrl(rawString);
if(clean.startsWith("http")) {
html.append("<a href=\"").append(clean).append("\">Åpne fil</a><br>");
text.append(clean).append("\n");
}
}
}
}
html.append("<br>");
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<JsonElement> 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<String, View> 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<List<GravityForm>>() {
@Override
public void onResponse(Call<List<GravityForm>> call, Response<List<GravityForm>> response) {
if (!isAdded()) return;
// Stopp lasting-indikatorer
progressBar.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null) {
List<GravityForm> allForms = response.body();
List<GravityForm> 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<GravityForm>() {
@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<List<GravityForm>> call, Throwable t) {
if (!isAdded()) return;
// Stopp lasting-indikatorer
progressBar.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false);
showError("Nettverksfeil: " + t.getMessage());
}
});
}
private void populateList(List<GravityForm> 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<String, String> inputValues;
public FormSubmission(Map<String, String> 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<Item> 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<Override> 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<Map<String, String>> entries;
// Vi bruker Map<String, String> 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<Choice> choices;
@SerializedName("content")
public String content;
// --- BRUKER ADAPTEREN HER ---
@JsonAdapter(InputsAdapter.class)
@SerializedName("inputs")
public List<GravityField> 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<String, String> 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<Rule> 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<GravityField> 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<HandbookAdapter.ViewHolder> {
private List<HandbookItem> fullList;
private List<HandbookItem> filteredList;
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(HandbookItem item);
}
public HandbookAdapter(List<HandbookItem> 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 =
"<style>" +
"body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #333333; line-height: 1.6; padding: 16px; margin: 0; }" +
"h1 { color: #0069B3; font-size: 24px; border-bottom: 2px solid #0069B3; padding-bottom: 10px; margin-top: 0; }" +
"p, ul, li { margin-bottom: 12px; }" +
"a { color: #0069B3; font-weight: bold; text-decoration: none; }" +
// --- BOKSEN ---
".trekkspill { " +
" background-color: #fff; " +
" border: 1px solid #ddd; " +
" border-radius: 4px; " +
" max-height: 58px; " + // LUKKET
" overflow: hidden; " +
" transition: max-height 0.4s ease; " +
// LUFT OG AVSTAND
" margin-top: 32px; " +
" margin-bottom: 16px; " +
"}" +
// --- KNAPPEN ---
".trekkspill > a[id^='fl-accordion--label-'] { " +
" display: flex; " +
" justify-content: space-between; " +
" align-items: center; " +
" background-color: #f2f2f2; " +
" color: #0069B3; " +
" padding: 15px; " +
" font-weight: bold; " +
" cursor: pointer; " +
" width: 100%; " +
" height: 58px; " +
" box-sizing: border-box; " +
" pointer-events: auto; " +
"}" +
// --- PLUSS-TEGN ---
".trekkspill > a[id^='fl-accordion--label-']::after { " +
" content: '+'; " +
" font-size: 24px; " +
" font-weight: bold; " +
" color: #0069B3; " +
"}" +
// --- ÅPEN TILSTAND ---
".trekkspill.open { " +
" max-height: 4000px; " + // ÅPEN
" overflow: visible; " +
" border-color: #0069B3; " +
"}" +
".trekkspill.open > a[id^='fl-accordion--label-'] { " +
" background-color: #0069B3; " +
" color: #ffffff; " +
"}" +
".trekkspill.open > a[id^='fl-accordion--label-']::after { " +
" content: '-'; " +
" color: #ffffff; " +
"}" +
// --- RYDDEJOBB ---
".trekkspill > a[id^='fl-accordion--icon-'] { display: none !important; }" +
".trekkspill > h2 { display: none; }" +
".trekkspill > p, .trekkspill > ul, .trekkspill > div { padding: 10px 15px; }" +
"</style>";
// --- JAVASCRIPT: AUTOSCROLL ---
private static final String JS_SCRIPT =
"<script>" +
"document.onclick = function(e) {\n" +
" var target = e.target.closest('a[id^=\"fl-accordion--label-\"]');\n" +
" \n" +
" if (target) {\n" +
" e.preventDefault();\n" +
" \n" +
" var currentBox = target.closest('.trekkspill');\n" +
" \n" +
" if (currentBox) {\n" +
" var wasOpen = currentBox.classList.contains('open');\n" +
" \n" +
" // LUKK ALLE ANDRE\n" +
" var allOpenBoxes = document.querySelectorAll('.trekkspill.open');\n" +
" allOpenBoxes.forEach(function(box) {\n" +
" box.classList.remove('open');\n" +
" });\n" +
" \n" +
" // ÅPNE DEN VALGTE\n" +
" if (!wasOpen) {\n" +
" currentBox.classList.add('open');\n" +
" \n" +
" // AUTOSCROLL: Økt til 300ms for å vente på CSS-animasjonen\n" +
" setTimeout(function() {\n" +
" currentBox.scrollIntoView({behavior: 'smooth', block: 'start'});\n" +
" }, 300);\n" +
" }\n" +
" }\n" +
" return false;\n" +
" }\n" +
"};" +
"</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<JsonObject>() {
@Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> 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<JsonObject> 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<HandbookPage>() {
@Override
public void onResponse(Call<HandbookPage> call, Response<HandbookPage> 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 = "<!DOCTYPE html><html><head>" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" +
CSS_STYLE +
JS_SCRIPT +
"</head><body>";
htmlContent += "<h1>" + page.title + "</h1>";
if (page.content != null) {
htmlContent += page.content;
} else {
htmlContent += "<p>Ingen innhold funnet.</p>";
}
htmlContent += "</body></html>";
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<HandbookPage> 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<List<HandbookItem>>() {
@Override
public void onResponse(Call<List<HandbookItem>> call, Response<List<HandbookItem>> 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<List<HandbookItem>> 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\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.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
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.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class HomeFragment extends Fragment {
private ActivityResultLauncher<String> requestPermissionLauncher;
private RecyclerView calendarRecycler;
private RecyclerView newsRecycler;
private ProgressBar mainProgressBar;
private SwipeRefreshLayout swipeRefreshLayout;
private int activeNetworkCalls = 0;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (calendarRecycler != null) {
refreshData();
}
}
);
// GAMMEL METODE FJERNET HERFRA (startNotificationWorker)
}
@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);
if (mainProgressBar != null) mainProgressBar.setVisibility(View.VISIBLE);
swipeRefreshLayout = view.findViewById(R.id.swipe_refresh_home);
swipeRefreshLayout.setOnRefreshListener(this::refreshData);
View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) {
profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile));
}
Button btnCreateEvent = view.findViewById(R.id.btn_create_event);
List<String> writeable = UserManager.getInstance().getWriteableCalendars();
if (writeable != null && !writeable.isEmpty()) {
btnCreateEvent.setVisibility(View.VISIBLE);
btnCreateEvent.setOnClickListener(v -> {
Navigation.findNavController(view).navigate(R.id.action_home_to_create_event);
});
} else {
btnCreateEvent.setVisibility(View.GONE);
}
calendarRecycler = view.findViewById(R.id.recycler_calendar);
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar);
if (viewAllCalendar != null) {
viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull));
}
if (android.os.Build.VERSION.SDK_INT >= 33) {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS);
}
}
newsRecycler = view.findViewById(R.id.recycler_news);
newsRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
newsRecycler.setNestedScrollingEnabled(false);
newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {}));
TextView viewAllNews = view.findViewById(R.id.btn_view_all_news);
if (viewAllNews != null) {
viewAllNews.setOnClickListener(v -> {
Navigation.findNavController(view).navigate(R.id.action_home_to_newsFull);
});
}
refreshData();
}
@Override
public void onResume() {
super.onResume();
if (activeNetworkCalls == 0) {
refreshData();
}
}
private void refreshData() {
activeNetworkCalls = 2;
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
fetchCalendarEvents(calendarRecycler);
} else {
checkLoadingComplete();
requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR);
}
fetchNewsFromWordpress(newsRecycler);
}
private void checkLoadingComplete() {
activeNetworkCalls--;
if (activeNetworkCalls <= 0) {
activeNetworkCalls = 0;
if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE);
if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false);
}
}
private void fetchCalendarEvents(RecyclerView recyclerView) {
new Thread(() -> {
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true);
new Handler(Looper.getMainLooper()).post(() -> fetchApiEvents(recyclerView, deviceEvents));
}).start();
}
private void fetchApiEvents(RecyclerView recyclerView, List<CalendarEvent> deviceEvents) {
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
apiEvents = response.body();
CacheManager.saveCalendarEvents(getContext(), apiEvents);
for (CalendarEvent e : apiEvents) {
CalendarManager.formatEventForUI(e);
}
} else {
apiEvents = CacheManager.getCachedCalendarEvents(getContext());
for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e);
if (!apiEvents.isEmpty()) {
Toast.makeText(getContext(), "Server utilgjengelig. Viser lagret kalender.", Toast.LENGTH_SHORT).show();
}
}
updateCalendarUI(recyclerView, apiEvents, deviceEvents);
checkLoadingComplete();
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
if (!isAdded()) return;
List<CalendarEvent> cachedApiEvents = CacheManager.getCachedCalendarEvents(getContext());
for (CalendarEvent e : cachedApiEvents) CalendarManager.formatEventForUI(e);
if (!cachedApiEvents.isEmpty()) {
Toast.makeText(getContext(), "Ingen nettverk. Viser lagret kalender.", Toast.LENGTH_SHORT).show();
}
updateCalendarUI(recyclerView, cachedApiEvents, deviceEvents);
checkLoadingComplete();
}
});
}
private void updateCalendarUI(RecyclerView recyclerView, List<CalendarEvent> apiEvents, List<CalendarEvent> deviceEvents) {
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
for (CalendarEvent e : merged) {
if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) {
upcomingEvents.add(e);
}
}
List<CalendarEvent> topEvents = new ArrayList<>();
for(int i=0; i<Math.min(upcomingEvents.size(), 5); i++) {
topEvents.add(upcomingEvents.get(i));
}
recyclerView.setAdapter(new CalendarAdapter(topEvents, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.setOnEventChangeListener(HomeFragment.this::refreshData);
sheet.show(getParentFragmentManager(), "CalendarDetails");
}));
}
private void fetchNewsFromWordpress(RecyclerView recyclerView) {
WordPressApiService apiService = RetrofitClient.getApiService();
apiService.getPosts().enqueue(new Callback<List<WpPost>>() {
@Override
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) {
if (getContext() == null) return;
List<WpPost> postsToShow = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
postsToShow = response.body();
CacheManager.saveNewsPosts(getContext(), postsToShow);
} else {
postsToShow = CacheManager.getCachedNewsPosts(getContext());
if (!postsToShow.isEmpty()) {
Toast.makeText(getContext(), "Server utilgjengelig. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show();
}
}
updateNewsUI(recyclerView, postsToShow);
checkLoadingComplete();
}
@Override
public void onFailure(Call<List<WpPost>> call, Throwable t) {
if (getContext() == null) return;
List<WpPost> cachedPosts = CacheManager.getCachedNewsPosts(getContext());
if (!cachedPosts.isEmpty()) {
Toast.makeText(getContext(), "Ingen nettverk. Viser lagrede nyheter.", Toast.LENGTH_SHORT).show();
}
updateNewsUI(recyclerView, cachedPosts);
checkLoadingComplete();
}
});
}
private void updateNewsUI(RecyclerView recyclerView, List<WpPost> posts) {
SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
for (WpPost post : posts) {
try {
Date date = rawFormat.parse(post.date);
post.date = targetFormat.format(date);
} catch (Exception e) {}
}
NewsAdapter adapter = new NewsAdapter(posts, post -> {
Bundle bundle = new Bundle();
bundle.putSerializable("post_data", post);
Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle);
});
recyclerView.setAdapter(adapter);
}
}
============================================================
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<android.graphics.drawable.Drawable>() {
@Override
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> 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<android.graphics.drawable.Drawable> 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<List<GravityField>> {
@Override
public List<GravityField> 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<GravityField> 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<JsonObject>() {
@Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> 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<JsonObject> 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<Intent> signInLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
Task<GoogleSignInAccount> 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<GoogleSignInAccount> 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<String> 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 androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
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.bottomnavigation.BottomNavigationView;
public class MainActivity extends AppCompatActivity {
public static final String GOOGLE_WEB_CLIENT_ID = "[SENSURERT].apps.googleusercontent.com"; // Bytt med din egen hvis denne er feil
private static final String TAG = "MainActivity";
private NavController navController;
private BottomNavigationView bottomNav;
private ActivityResultLauncher<String> requestPermissionLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// --- 1. SETUP UI & NAVIGASJON ---
bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
if (bottomNav != null) {
NavigationUI.setupWithNavController(bottomNav, navController);
// Håndter "Reselection" (Klikk på fanen man allerede er i)
bottomNav.setOnItemReselectedListener(item -> {
navController.popBackStack(item.getItemId(), false);
});
}
// Skjul meny på login-skjerm
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
if (bottomNav == null) return;
if (destination.getId() == R.id.navigation_login) {
bottomNav.setVisibility(View.GONE);
} else {
bottomNav.setVisibility(View.VISIBLE);
}
});
}
// --- 2. VARSLINGSOPPSETT ---
createNotificationChannel();
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
Log.d(TAG, "Varslingstillatelse gitt!");
} else {
Log.e(TAG, "Varslingstillatelse avslått.");
}
});
checkNotificationPermission();
checkExactAlarmPermission();
// GAMMEL METODE FJERNET HERFRA (NotificationWorker)
// --- 3. AUTENTISERING ---
checkLoginState();
}
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) {
Log.d(TAG, "Silent login fullført. Rolle: " + role);
if (navController != null && navController.getCurrentDestination() != null &&
navController.getCurrentDestination().getId() == R.id.navigation_login) {
navController.navigate(R.id.action_login_to_home);
}
}
@Override
public void onError(String message) {
Log.e(TAG, "Silent login feilet mot WP: " + message);
navigateToLogin();
}
}
);
})
.addOnFailureListener(e -> {
Log.e(TAG, "Silent Sign-In feilet mot Google", e);
navigateToLogin();
});
}
private void navigateToLogin() {
if (navController != null) {
if (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) {
CharSequence name = "KBS Kalendervarsler";
String description = "Varsler for kalenderhendelser";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.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()) {
new AlertDialog.Builder(this)
.setTitle("Varslingstillatelse kreves")
.setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.")
.setPositiveButton("Gå til Innstillinger", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.setNegativeButton("Senere", null)
.show();
}
}
}
}
============================================================
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);
Log.d(TAG, "Ny FCM Token: " + token);
AuthRepository.updateDeviceToken(token);
}
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
Log.d(TAG, "Melding mottatt fra: " + remoteMessage.getFrom());
// 1. Sjekk data payload (Bakgrunnsoppdatering)
if (remoteMessage.getData().size() > 0) {
String forceRefresh = remoteMessage.getData().get("force_refresh");
if ("true".equalsIgnoreCase(forceRefresh)) {
Log.d(TAG, "Mottok 'force_refresh' - oppdaterer kalender og alarmer...");
updateCalendarAndAlarms();
}
// Hvis meldingen også har egne titler i data-feltet (valgfritt)
String title = remoteMessage.getData().get("title");
String body = remoteMessage.getData().get("body");
if (title != null && body != null) {
showNotification(title, body);
}
}
// 2. Sjekk notification payload (Vises automatisk når app er i bakgrunn, men vi håndterer den her for forgrunn)
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Melding varsel body: " + remoteMessage.getNotification().getBody());
showNotification(
remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody()
);
}
}
private void updateCalendarAndAlarms() {
// Vi bruker Retrofit for å hente kalenderen på nytt
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (response.isSuccessful() && response.body() != null) {
// Lagre til cache først (god praksis)
CacheManager.saveCalendarEvents(getApplicationContext(), response.body());
// Oppdater alarmer lokalt
AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body());
Log.d(TAG, "Kalender og alarmer oppdatert via Push.");
}
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
Log.e(TAG, "Feil ved push-oppdatering av kalender", 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 notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify((int) System.currentTimeMillis(), builder.build());
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"KBS Kalendervarsler",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Varsler fra KBS Intranett");
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.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<NewsAdapter.ViewHolder> {
private List<WpPost> posts;
private OnItemClickListener listener; // NYTT
// Interface for klikk
public interface OnItemClickListener {
void onItemClick(WpPost post);
}
// Oppdatert konstruktør
public NewsAdapter(List<WpPost> posts, OnItemClickListener listener) {
this.posts = posts;
this.listener = listener;
}
// Overload for bakoverkompatibilitet (hvis du ikke sender listener)
public NewsAdapter(List<WpPost> 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<WpPost> 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 =
"<style>" +
"body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #333333; line-height: 1.6; padding: 0; margin: 0; }" +
"p, ul, li { margin-bottom: 12px; font-size: 16px; }" +
"a { color: #0069B3; font-weight: bold; text-decoration: none; }" +
"h1, h2, h3 { color: #0069B3; margin-top: 20px; margin-bottom: 10px; }" +
// Bilde-styling
"img { " +
" max-width: 100% !important; " +
" height: auto !important; " +
" border-radius: 4px; " +
" margin: 16px 0; " +
" box-shadow: 0 2px 5px rgba(0,0,0,0.1); " +
" display: block;" +
"}" +
// Fjerner unødvendig whitespace fra WP-galleri
".gallery-item { margin: 0; padding: 0; }" +
"</style>";
// JavaScript for å fange opp bildeklikk
private static final String JS_SCRIPT =
"<script>" +
"document.addEventListener('DOMContentLoaded', function() {" +
" var images = document.getElementsByTagName('img');" +
" for (var i = 0; i < images.length; i++) {" +
" images[i].onclick = function() {" +
" Android.showImage(this.src);" +
" }" +
" }" +
"});" +
"</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 = "<!DOCTYPE html><html><head>" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" +
CSS_STYLE +
JS_SCRIPT +
"</head><body>" +
rawContent +
"</body></html>";
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.ImageView;
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<WpPost> allPosts = new ArrayList<>(); // Holder på ALLE postene
@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);
ImageView backBtn = view.findViewById(R.id.btn_back_news);
// Setup Nyhetsliste
recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext()));
// Setup Kategorier (Horisontal)
recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
setupCategories();
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
fetchAllNews();
}
private void setupCategories() {
// Listen over kategorier du ønsket
List<String> categories = Arrays.asList(
"Alle", // Standard vis alt
"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);
// Hent 50 siste (bør holde for en "Siste nytt" liste, ellers må vi paginere)
RetrofitClient.getApiService().getAllPosts().enqueue(new Callback<List<WpPost>>() {
@Override
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
if (response.isSuccessful() && response.body() != null) {
allPosts = response.body();
formatDates(allPosts);
// Vis alle i starten
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<List<WpPost>> 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<WpPost> filteredList = new ArrayList<>();
if (category.equals("Alle")) {
filteredList.addAll(allPosts);
} else {
for (WpPost post : allPosts) {
// Vi sjekker om kategorinavnet matcher
if (post.getCategoryName().equals(category)) {
filteredList.add(post);
}
}
}
// Oppdater adapteren med den filtrerte listen
newsAdapter.updateList(filteredList);
}
private void formatDates(List<WpPost> posts) {
SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
for (WpPost post : posts) {
try {
Date date = rawFormat.parse(post.date);
post.date = targetFormat.format(date);
} catch (Exception e) {}
}
}
}
============================================================
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\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<List<GravityField.Choice>>(){}.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\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<String> 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<String> 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\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<String> 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<String> calendars) {
this.writeableCalendars = calendars != null ? calendars : new ArrayList<>();
}
public List<String> 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 {
@GET("wp-json/wp/v2/posts?per_page=10&_embed")
Call<List<WpPost>> getPosts();
@GET("wp-json/kbs/v1/forms/{id}")
Call<GravityForm> getForm(@Path("id") int formId);
@POST("wp-json/gf/v2/forms/{id}/submissions")
Call<JsonElement> submitForm(@Path("id") int formId, @Body FormSubmission submission);
@POST("wp-json/kbs/v1/login")
Call<LoginResponse> googleLogin(@Body LoginRequest request);
@GET("wp-json/kbs/v1/forms")
Call<List<GravityForm>> getFormsList();
@Multipart
@POST("wp-json/gf/v2/forms/{id}/submissions")
Call<JsonElement> submitMultipartForm(
@Path("id") int formId,
@PartMap Map<String, RequestBody> textFields,
@Part List<MultipartBody.Part> files
);
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
@POST("wp-json/kbs/v1/calendar/create")
Call<JsonElement> createCalendarEvent(@Body CreateEventRequest request);
// NYTT ENDEPUNKT
@GET("wp-json/kbs/v1/users")
Call<List<User>> getUsersList();
@GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> 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<JsonElement> getSingleEntry(@Path("entry_id") String entryId);
@GET("wp-json/wp/v2/posts?per_page=50&_embed")
Call<List<WpPost>> getAllPosts();
@GET("wp-json/kbs/v1/handbook")
Call<List<HandbookItem>> getHandbookItems();
@GET("wp-json/kbs/v1/handbook/{id}")
Call<HandbookPage> getHandbookPage(@Path("id") int id);
@GET("wp-json/kbs/v1/lookup-id")
Call<JsonObject> lookupPageId(@Query("url") String url);
@POST("wp-json/kbs/v1/calendar/update")
Call<JsonElement> updateCalendarEvent(@Body CreateEventRequest request);
@POST("wp-json/kbs/v1/calendar/delete")
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request);
@POST("wp-json/kbs/v1/device/register")
Call<JsonElement> registerDevice(@Body RegisterDeviceRequest request);
}
============================================================
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<Media> mediaList;
@SerializedName("wp:term")
public List<List<Term>> termList;
// NYTT: Forfatter-liste
@SerializedName("author")
public List<Author> 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<Term> categories = embedded.termList.get(0);
if (categories == null || categories.isEmpty()) return "";
List<String> 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
============================================================
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="#FFFFFF"/>
<item android:color="#333333"/>
</selector>
============================================================
FILSTI: app\src\main\res\drawable\bg_category_selected.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/kbs_logo_blue" />
<corners android:radius="20dp" />
</shape>
============================================================
FILSTI: app\src\main\res\drawable\bg_category_unselected.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#DDDDDD" />
<corners android:radius="20dp" />
</shape>
============================================================
FILSTI: app\src\main\res\drawable\bg_date_box.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/kbs_logo_light_blue"/>
<corners android:radius="8dp"/>
</shape>
============================================================
FILSTI: app\src\main\res\drawable\ic_book.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_form.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M22,7h-9v2h9V7zM22,15h-9v2h9V15zM5.54,11L2,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,11zM5.54,19L2,15.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,19z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_car.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11c-0.66,0 -1.21,0.42 -1.42,1.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8l-2.08,-5.99zM6.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,13 6.5,13s1.5,0.67 1.5,1.5S7.33,16 6.5,16zM17.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM5,11l1.5,-4.5h11L19,11H5z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_doc.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6zm2,16H8v-2h8v2zm0,-4H8v-2h8v2zm-3,-5V3.5L18.5,9H13z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_general.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2V7h-2v2z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_health.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM17,13h-4v4h-2v-4H7v-2h4V7h2v4h4V13z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_people.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zm-8,0c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zm0,2c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zm8,0c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_handbook_warning.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#0069B3">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zm12,-3h-2v-2h2v2zm0,-4h-2v-4h2v4z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_home.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_launcher_background.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
============================================================
FILSTI: app\src\main\res\drawable\ic_launcher_foreground.xml
============================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
============================================================
FILSTI: app\src\main\res\drawable\selector_day_toggle.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<shape android:shape="oval">
<solid android:color="#0069B3"/> <!-- KBS Blå -->
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="#EEEEEE"/> <!-- Lys grå -->
</shape>
</item>
</selector>
============================================================
FILSTI: app\src\main\res\layout\activity_main.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:menu="@menu/bottom_nav_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
============================================================
FILSTI: app\src\main\res\layout\activity_webview.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Light" />
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@android:color/white">
<!-- Kalendernavn (Lite merke øverst) -->
<TextView
android:id="@+id/sheet_calendar_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KALENDERNAVN"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="@color/white"
android:background="@drawable/bg_date_box"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:layout_marginBottom="12dp"/>
<TextView
android:id="@+id/sheet_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tittel"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/sheet_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tidspunkt"
android:textSize="16sp"
android:textColor="@color/kbs_muted_blue_gray"
android:layout_marginBottom="12dp"
android:drawablePadding="8dp"
app:drawableStartCompat="@android:drawable/ic_menu_recent_history"/>
<TextView
android:id="@+id/sheet_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sted"
android:textSize="16sp"
android:textColor="@color/kbs_logo_blue"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:visibility="gone"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:paddingVertical="8dp"
app:drawableStartCompat="@android:drawable/ic_dialog_map"/>
<!-- NYTT: Arrangør -->
<TextView
android:id="@+id/sheet_organizer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Invitert av: ..."
android:textSize="14sp"
android:textColor="#333"
android:background="#F5F5F5"
android:padding="12dp"
android:layout_marginBottom="4dp"
android:visibility="gone"/>
<!-- Deltakere -->
<TextView
android:id="@+id/sheet_participants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Synlig for: ..."
android:textSize="14sp"
android:textColor="#333"
android:background="#F5F5F5"
android:padding="12dp"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<TextView
android:id="@+id/sheet_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Beskrivelse..."
android:textSize="14sp"
android:textColor="#555"
android:layout_marginBottom="24dp"
android:autoLink="web|email|phone"
android:linksClickable="true"/>
<!-- ADMIN KNAPPER (Vises kun for admin) -->
<LinearLayout
android:id="@+id/layout_admin_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Slett"
android:backgroundTint="#D32F2F"
android:textColor="#FFF"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btn_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Endre"
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="#FFF"
android:layout_marginStart="8dp"/>
</LinearLayout>
<Button
android:id="@+id/btn_add_to_calendar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\dialog_custom_recurrence.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Egendefinert gjentakelse"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#333"
android:layout_marginBottom="24dp"/>
<!-- FREKVENS -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gjenta hvert: "
android:textSize="16sp"/>
<EditText
android:id="@+id/et_interval"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:inputType="number"
android:text="1"
android:gravity="center"
android:layout_marginHorizontal="8dp"/>
<Spinner
android:id="@+id/spinner_freq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/recurrence_freq_array"/>
</LinearLayout>
<!-- UKEDAGER (Vises kun hvis Uke er valgt) -->
<LinearLayout
android:id="@+id/layout_weekdays"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"
android:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gjenta på"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="7">
<ToggleButton android:id="@+id/tg_mon" android:textOn="M" android:textOff="M" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_tue" android:textOn="T" android:textOff="T" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_wed" android:textOn="O" android:textOff="O" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_thu" android:textOn="T" android:textOff="T" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_fri" android:textOn="F" android:textOff="F" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_sat" android:textOn="L" android:textOff="L" style="@style/DayToggle"/>
<ToggleButton android:id="@+id/tg_sun" android:textOn="S" android:textOff="S" style="@style/DayToggle"/>
</LinearLayout>
</LinearLayout>
<!-- SLUTTER -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Slutter"
android:layout_marginBottom="8dp"/>
<RadioGroup
android:id="@+id/rg_end"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/rb_never"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Aldri"
android:checked="true"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<RadioButton
android:id="@+id/rb_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Den"/>
<Button
android:id="@+id/btn_end_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Velg dato"
android:layout_marginStart="16dp"
style="@style/Widget.MaterialComponents.Button.TextButton"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<RadioButton
android:id="@+id/rb_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Etter"/>
<EditText
android:id="@+id/et_count"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:inputType="number"
android:text="13"
android:gravity="center"
android:layout_marginHorizontal="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ganger"/>
</LinearLayout>
</RadioGroup>
<!-- KNAPPER -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="32dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Avbryt"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:textColor="#666"/>
<Button
android:id="@+id/btn_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ferdig"
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="#FFF"
android:layout_marginStart="16dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
============================================================
FILSTI: app\src\main\res\layout\fragment_calendar_full.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/kbs_very_light_blue">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@color/white"
android:elevation="4dp">
<ImageView
android:id="@+id/btn_back_calendar"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_revert"
android:layout_centerVertical="true"
app:tint="@color/kbs_logo_blue"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KBS Kalender"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_centerInParent="true"/>
</RelativeLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_full_calendar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false"/>
<ProgressBar
android:id="@+id/loading_full_calendar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<TextView
android:id="@+id/empty_view_calendar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ingen hendelser funnet"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_create_event.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Opprett ny hendelse"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="24dp"
android:textColor="#333"/>
<!-- TITTEL -->
<EditText
android:id="@+id/et_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Tittel"
android:inputType="textCapSentences"
android:padding="12dp"
android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/>
<!-- BESKRIVELSE -->
<EditText
android:id="@+id/et_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Beskrivelse / Notater"
android:inputType="textMultiLine"
android:minLines="3"
android:padding="12dp"
android:gravity="top"
android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/>
<!-- STED -->
<EditText
android:id="@+id/et_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Hvor (Sted)"
android:inputType="textCapWords"
android:padding="12dp"
android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp"/>
<!-- KALENDER VALG -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Velg Kalender:"
android:textSize="14sp"
android:textColor="#666"/>
<Spinner
android:id="@+id/spinner_calendar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="12dp"/>
<!-- SYNLIGHET / DELTAKERE (NYTT) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Synlighet / Deltakere:"
android:textSize="14sp"
android:textColor="#666"
android:layout_marginTop="8dp"/>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp">
<RadioButton
android:id="@+id/rb_visibility_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alle i kalenderen (Standard)"
android:checked="true"/>
<RadioButton
android:id="@+id/rb_visibility_specific"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Begrens til valgte personer..."/>
</RadioGroup>
<Button
android:id="@+id/btn_select_participants"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Velg personer"
android:visibility="gone"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/txt_selected_participants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ingen valgt"
android:textSize="12sp"
android:textStyle="italic"
android:visibility="gone"
android:layout_marginBottom="16dp"/>
<!-- HELE DAGEN -->
<Switch
android:id="@+id/switch_all_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hele dagen"
android:layout_marginBottom="16dp"/>
<!-- START DATO/TID -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/btn_start_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Startdato"
android:layout_marginEnd="4dp"/>
<Button
android:id="@+id/btn_start_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Starttid"
android:layout_marginStart="4dp"/>
</LinearLayout>
<!-- SLUTT DATO/TID -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<Button
android:id="@+id/btn_end_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sluttdato"
android:layout_marginEnd="4dp"/>
<Button
android:id="@+id/btn_end_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Slutttid"
android:layout_marginStart="4dp"/>
</LinearLayout>
<TextView
android:id="@+id/txt_time_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Valgt: -"
android:gravity="center"
android:textStyle="bold"
android:layout_marginBottom="24dp"/>
<!-- GJENTAKELSE -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gjentakelse:"
android:textSize="14sp"
android:textColor="#666"/>
<Spinner
android:id="@+id/spinner_recurrence"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="12dp"/>
<!-- VARSLING MED CHIPS -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Varsling (Velg en eller flere):"
android:textSize="14sp"
android:textColor="#666"
android:layout_marginBottom="8dp"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_marginBottom="32dp">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_reminders"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleLine="true"
app:selectionRequired="false">
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<Button
android:id="@+id/btn_save_event"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Lagre i Kalender"
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="#FFFFFF"/>
</LinearLayout>
</ScrollView>
============================================================
FILSTI: app\src\main\res\layout\fragment_forms.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5"
tools:context=".FormsFragment">
<!-- NYTT: Toolbar med tilbake-knapp -->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Light">
<androidx.appcompat.widget.Toolbar
android:id="@+id/forms_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
app:navigationIcon="@android:drawable/ic_menu_revert"
app:titleTextColor="@color/black"
app:title="Laster..." />
</com.google.android.material.appbar.AppBarLayout>
<!-- Resten er likt, men nå under toolbaren -->
<LinearLayout
android:id="@+id/history_wrapper"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp"
android:layout_marginBottom="8dp"
android:padding="16dp">
<TextView
android:id="@+id/lbl_history"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tidligere innsendinger"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="10dp"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/historyContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="7"
android:orientation="vertical"
android:background="#FFFFFF"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FAFAFA">
<ProgressBar
android:id="@+id/loading_spinner"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/txt_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="#666666"
android:textSize="12sp"
android:text="" />
<ImageView
android:id="@+id/btn_toggle_history"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/arrow_up_float"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:contentDescription="Vis/Skjul historikk"
app:tint="#666666" />
</LinearLayout>
<ScrollView
android:id="@+id/form_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fillViewport="true">
<LinearLayout
android:id="@+id/form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="40dp">
</LinearLayout>
</ScrollView>
</LinearLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_forms_list.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#F5F5F5">
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skjemaer"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="20dp"
android:textColor="#333333"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/forms_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_handbook.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/white"
android:elevation="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Håndbok"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="KBS Klima og Byggservice ønsker å ta vare på miljø og helse, og har derfor utarbeidet en håndbok som omhandler disse temaene. For enkelthets skyld er denne publisert i KBS-appen og på Intranettet.\n\nDet forventes at alle ansatte skal ha lest hele håndboken minst én gang, og at de har lest og forstått bedriftens HMS-målsetting senest 1. april hvert år."
android:textSize="14sp"
android:textColor="#666666"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="16dp"/>
<EditText
android:id="@+id/search_field"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/bg_category_unselected"
android:hint="Søk i håndboken..."
android:paddingHorizontal="16dp"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:textColor="#333"
android:textSize="14sp"
android:inputType="text"/>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_handbook"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false"/>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_handbook_detail.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/white">
<androidx.appcompat.widget.Toolbar
android:id="@+id/detail_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/kbs_very_light_blue"
android:elevation="4dp"
app:navigationIcon="@android:drawable/ic_menu_revert"
app:titleTextColor="@color/black" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/detail_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<ProgressBar
android:id="@+id/detail_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_home.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/kbs_very_light_blue">
<!-- SwipeRefreshLayout må pakke inn det skrollbare innholdet -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_home"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<!-- OVERSKRIFT OG PROFIL -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingHorizontal="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KBS Intranett"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray"
android:layout_centerVertical="true"/>
<ImageView
android:id="@+id/btn_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_my_calendar"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
app:tint="@color/kbs_logo_blue"/>
</RelativeLayout>
<Button
android:id="@+id/btn_create_event"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="+ Ny Kalenderhendelse"
android:backgroundTint="@color/kbs_logo_blue"
android:textColor="#FFFFFF"
android:layout_marginHorizontal="8dp"
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- KALENDER-SEKSJON -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Kommende hendelser"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btn_view_all_calendar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Se alle >"
android:textColor="@color/kbs_logo_blue"
android:textStyle="bold"
android:padding="8dp"
android:background="?attr/selectableItemBackground"/>
</LinearLayout>
<!-- ENDRET: Fast høyde for å skape "vindu" med scroll -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_calendar"
android:layout_width="match_parent"
android:layout_height="190dp"
android:scrollbars="vertical"
android:layout_marginBottom="16dp"/>
<!-- NYHETER-SEKSJON -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Siste nytt"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btn_view_all_news"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Se alle >"
android:textColor="@color/kbs_logo_blue"
android:textStyle="bold"
android:padding="8dp"
android:background="?attr/selectableItemBackground"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_news"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:scrollbars="vertical"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- Initial Loader -->
<ProgressBar
android:id="@+id/main_loading_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"/>
</FrameLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_image_dialog.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<!-- Lukkeknapp -->
<ImageButton
android:id="@+id/btn_close_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_margin="16dp"
app:tint="@android:color/white"
android:contentDescription="Lukk bildevisning"
android:elevation="10dp"/>
<!-- Selve bildet -->
<ImageView
android:id="@+id/full_screen_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="Fullskjermbilde" />
<!-- Loading spinner -->
<ProgressBar
android:id="@+id/loading_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_login.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:background="#FFFFFF">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KBS Intranett"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Vennligst logg inn"
android:textSize="16sp"
android:textColor="#666666"
android:layout_marginBottom="48dp"/>
<com.google.android.gms.common.SignInButton
android:id="@+id/sign_in_button"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text=""
android:textColor="#D32F2F"/>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_news_detail.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="@color/kbs_logo_blue">
<ImageView
android:id="@+id/detail_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/detail_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@android:drawable/ic_menu_revert" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Kategori -->
<TextView
android:id="@+id/detail_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/kbs_logo_accent_red"
android:textStyle="bold"
android:textAllCaps="true"
android:textSize="12sp"
android:layout_marginBottom="8dp"/>
<!-- Tittel -->
<TextView
android:id="@+id/detail_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"/>
<!-- Dato og Forfatter -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="24dp">
<TextView
android:id="@+id/detail_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/detail_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textStyle="italic"
android:textSize="12sp"
android:text="Av: Forfatter"/>
</LinearLayout>
<!--
WebView for innhold.
tools:ignore="WebViewLayout" hindrer feilmeldingen om wrap_content,
da dette er ønsket oppførsel inne i en NestedScrollView.
-->
<WebView
android:id="@+id/detail_webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
tools:ignore="WebViewLayout" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_news_full.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/kbs_very_light_blue">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@android:color/white"
android:elevation="4dp">
<ImageView
android:id="@+id/btn_back_news"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_revert"
android:layout_centerVertical="true"
android:contentDescription="Tilbake"
android:background="?attr/selectableItemBackgroundBorderless"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Siste nytt"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_centerInParent="true"/>
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:clipToPadding="false"
android:scrollbars="none"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_news_full"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:paddingHorizontal="4dp"
android:clipToPadding="false"/>
<ProgressBar
android:id="@+id/loading_news_full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\fragment_profile.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/kbs_very_light_blue">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:paddingHorizontal="10dp"
android:background="@color/kbs_logo_blue" >
<ImageView
android:id="@+id/btn_close_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
app:tint="@color/white"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min Profil"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/white"
android:layout_centerInParent="true"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingTop="32dp">
<ImageView
android:id="@+id/profile_image"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@android:drawable/sym_def_app_icon"
android:background="@android:color/transparent"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Laster navn..."
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray"/>
<TextView
android:id="@+id/profile_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="..."
android:textSize="16sp"
android:textColor="@color/kbs_muted_blue_gray"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/profile_role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rolle: ..."
android:textSize="14sp"
android:textStyle="italic"
android:textColor="@color/kbs_logo_blue"
android:background="@color/kbs_very_light_blue"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"/>
<Button
android:id="@+id/btn_update_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Oppdater mine opplysninger"
android:backgroundTint="@color/white"
android:textColor="@color/kbs_logo_blue"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/btn_logout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Logg ut"
android:backgroundTint="@color/kbs_logo_accent_red"
android:textColor="@color/white"
android:layout_marginBottom="32dp"/>
<!-- NYTT: VERSJONSINFO -->
<TextView
android:id="@+id/tv_version_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Versjon 1.0"
android:textColor="#999999"
android:textSize="12sp"/>
</LinearLayout>
</LinearLayout>
============================================================
FILSTI: app\src\main\res\layout\item_calendar.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginHorizontal="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/date_box_background"
android:layout_width="50dp"
android:layout_height="50dp"
android:orientation="vertical"
android:gravity="center"
android:background="@drawable/bg_date_box"
android:layout_marginEnd="16dp">
<TextView
android:id="@+id/cal_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/white"/>
<TextView
android:id="@+id/cal_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DES"
android:textSize="10sp"
android:textAllCaps="true"
android:textColor="@color/white"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/cal_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Møtetittel"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/kbs_muted_blue_gray"/>
<TextView
android:id="@+id/cal_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Kl. 10:00 - 11:00"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
============================================================
FILSTI: app\src\main\res\layout\item_category.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/category_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kategori"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_category_unselected"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold" />
============================================================
FILSTI: app\src\main\res\layout\item_handbook.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_handbook_general"
app:tint="@color/kbs_logo_blue"
android:layout_marginBottom="12dp"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tittel"
android:textStyle="bold"
android:textColor="#333333"
android:textSize="14sp"
android:gravity="center"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Beskrivelse..."
android:textColor="#666666"
android:textSize="12sp"
android:gravity="center"
android:maxLines="2"
android:ellipsize="end"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
============================================================
FILSTI: app\src\main\res\layout\item_news.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginHorizontal="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
app:cardBackgroundColor="@android:color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/news_image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@android:drawable/ic_menu_gallery"
android:background="#EEEEEE" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/news_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="KATEGORI"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="@color/kbs_logo_accent_red"
android:textAllCaps="true"
android:letterSpacing="0.05"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Overskrift på nyhetssaken kommer her"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:lineSpacingExtra="2dp"/>
<TextView
android:id="@+id/news_excerpt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Her kommer en kort ingress som beskriver saken litt nærmere før man klikker seg inn..."
android:textColor="#555555"
android:textSize="14sp"
android:maxLines="3"
android:ellipsize="end"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@android:drawable/ic_menu_my_calendar"
app:tint="#999999"
android:layout_marginEnd="6dp"/>
<TextView
android:id="@+id/news_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="23. Nov 2025"
android:textSize="12sp"
android:textColor="#999999"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
============================================================
FILSTI: app\src\main\res\menu\bottom_nav_menu.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@android:drawable/ic_menu_today"
android:title="Hjem" />
<item
android:id="@+id/navigation_forms"
android:icon="@android:drawable/ic_menu_edit"
android:title="Skjema" />
<item
android:id="@+id/navigation_handbook"
android:icon="@android:drawable/ic_menu_info_details"
android:title="Håndbok" />
</menu>
============================================================
FILSTI: app\src\main\res\mipmap-anydpi-v26\ic_launcher.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
============================================================
FILSTI: app\src\main\res\mipmap-anydpi-v26\ic_launcher_round.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
============================================================
FILSTI: app\src\main\res\navigation\mobile_navigation.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_login"
android:name="com.kbs.kbsintranett.LoginFragment"
android:label="Logg inn"
tools:layout="@layout/fragment_login">
<action
android:id="@+id/action_login_to_home"
app:destination="@id/navigation_home"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/navigation_home"
android:name="com.kbs.kbsintranett.HomeFragment"
android:label="Hjem"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_home_to_calendarFull"
app:destination="@id/navigation_calendar_full" />
<action
android:id="@+id/action_home_to_newsFull"
app:destination="@id/navigation_news_full" />
<action
android:id="@+id/action_home_to_newsDetail"
app:destination="@id/navigation_news_detail" />
<!-- HER ER DEN MANGLENDE LINKEN: -->
<action
android:id="@+id/action_home_to_create_event"
app:destination="@id/navigation_create_event" />
</fragment>
<!-- NYTT FRAGMENT: Opprett hendelse -->
<fragment
android:id="@+id/navigation_create_event"
android:name="com.kbs.kbsintranett.CreateEventFragment"
android:label="Ny Hendelse"
tools:layout="@layout/fragment_create_event" />
<fragment
android:id="@+id/navigation_calendar_full"
android:name="com.kbs.kbsintranett.CalendarFullFragment"
android:label="Kalender"
tools:layout="@layout/fragment_calendar_full" />
<fragment
android:id="@+id/navigation_news_full"
android:name="com.kbs.kbsintranett.NewsFullFragment"
android:label="Nyheter"
tools:layout="@layout/fragment_news_full">
<action
android:id="@+id/action_newsFull_to_newsDetail"
app:destination="@id/navigation_news_detail" />
</fragment>
<fragment
android:id="@+id/navigation_news_detail"
android:name="com.kbs.kbsintranett.NewsDetailFragment"
android:label="Nyhet"
tools:layout="@layout/fragment_news_detail" />
<fragment
android:id="@+id/navigation_forms"
android:name="com.kbs.kbsintranett.FormsListFragment"
android:label="Skjemaer"
tools:layout="@layout/fragment_forms_list">
<action
android:id="@+id/action_formsListFragment_to_formsDetailFragment"
app:destination="@id/navigation_forms_detail" />
</fragment>
<fragment
android:id="@+id/navigation_forms_detail"
android:name="com.kbs.kbsintranett.FormsFragment"
android:label="Fyll ut skjema"
tools:layout="@layout/fragment_forms">
<argument
android:name="formId"
app:argType="integer"
android:defaultValue="0" />
</fragment>
<fragment
android:id="@+id/navigation_handbook"
android:name="com.kbs.kbsintranett.HandbookFragment"
android:label="Håndbok"
tools:layout="@layout/fragment_handbook">
<action
android:id="@+id/action_handbook_to_detail"
app:destination="@id/navigation_handbook_detail" />
</fragment>
<fragment
android:id="@+id/navigation_handbook_detail"
android:name="com.kbs.kbsintranett.HandbookDetailFragment"
android:label="Håndbok Detaljer"
tools:layout="@layout/fragment_handbook_detail">
<argument
android:name="page_id"
app:argType="integer" />
<argument
android:name="page_title"
app:argType="string" />
<action
android:id="@+id/action_handbook_to_detail"
app:destination="@id/navigation_handbook_detail" />
<action
android:id="@+id/action_handbook_to_form"
app:destination="@id/navigation_forms_detail" />
</fragment>
<fragment
android:id="@+id/navigation_profile"
android:name="com.kbs.kbsintranett.ProfileFragment"
android:label="Min Profil"
tools:layout="@layout/fragment_profile">
<action
android:id="@+id/action_profile_to_login"
app:destination="@id/navigation_login"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_profile_to_form"
app:destination="@id/navigation_forms_detail" />
</fragment>
</navigation>
============================================================
FILSTI: app\src\main\res\values\colors.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="kbs_logo_blue">#0069B3</color>
<color name="kbs_logo_light_blue">#53AFE9</color>
<color name="kbs_logo_accent_red">#C40426</color>
<color name="kbs_very_light_blue">#F5F7FA</color>
<color name="kbs_muted_blue_gray">#4F5B66</color>
<color name="kbs_soft_light_pink_beige">#F8E5E8</color>
</resources>
============================================================
FILSTI: app\src\main\res\values\ic_launcher_background.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
============================================================
FILSTI: app\src\main\res\values\strings.xml
============================================================
<resources>
<string name="app_name">KBS Intranett</string>
<!-- NYTT: Array for gjentakelse-spinneren -->
<string-array name="recurrence_freq_array">
<item>dag</item>
<item>uke</item>
<item>måned</item>
<item>år</item>
</string-array>
</resources>
============================================================
FILSTI: app\src\main\res\values\themes.xml
============================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Base.Theme.KBSIntranett" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">#0056b3</item> <item name="colorPrimaryVariant">#004494</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.KBSIntranett" parent="Base.Theme.KBSIntranett" />
<!-- NYTT: Stil for runde ukedag-knapper (M T O T F L S) -->
<style name="DayToggle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:layout_weight">1</item>
<item name="android:background">@drawable/selector_day_toggle</item>
<item name="android:textColor">@color/selector_day_text</item>
<item name="android:textOff">M</item>
<item name="android:textOn">M</item>
<item name="android:textSize">12sp</item>
<item name="android:layout_margin">2dp</item>
</style>
</resources>
============================================================
FILSTI: app\src\main\res\values-night\themes.xml
============================================================
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Base.Theme.KBSIntranett" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">#0056b3</item> <item name="colorPrimaryVariant">#004494</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.KBSIntranett" parent="Base.Theme.KBSIntranett" />
</resources>
============================================================
FILSTI: app\src\main\res\xml\backup_rules.xml
============================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
============================================================
FILSTI: app\src\main\res\xml\data_extraction_rules.xml
============================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
============================================================
FILSTI: app\src\main\res\xml\file_paths.xml
============================================================
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="my_images" path="Pictures" />
</paths>
============================================================
FILSTI: app\src\test\java\com\kbs\kbsintranett\ExampleUnitTest.java
============================================================
package com.kbs.kbsintranett;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}