11190 lines
440 KiB
Text
11190 lines
440 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
|
||
|
|
============================================================
|
||
|
|
import java.util.Properties
|
||
|
|
import java.io.FileInputStream
|
||
|
|
|
||
|
|
plugins {
|
||
|
|
alias(libs.plugins.android.application)
|
||
|
|
// NY LINJE: Aktiver Google Services plugin her
|
||
|
|
id("com.google.gms.google-services")
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- NY KODE: Last inn local.properties ---
|
||
|
|
// Vi bruker "Properties()" direkte siden vi har importert den på toppen
|
||
|
|
val localProperties = Properties()
|
||
|
|
val localPropertiesFile = rootProject.file("local.properties")
|
||
|
|
if (localPropertiesFile.exists()) {
|
||
|
|
localProperties.load(FileInputStream(localPropertiesFile))
|
||
|
|
}
|
||
|
|
// ------------------------------------------
|
||
|
|
|
||
|
|
android {
|
||
|
|
namespace = "com.kbs.kbsintranett"
|
||
|
|
compileSdk = 34
|
||
|
|
|
||
|
|
defaultConfig {
|
||
|
|
applicationId = "com.kbs.kbsintranett"
|
||
|
|
minSdk = 28
|
||
|
|
targetSdk = 34
|
||
|
|
versionCode = 4
|
||
|
|
versionName = "1.5.1"
|
||
|
|
|
||
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||
|
|
|
||
|
|
// Hent verdien vi lastet inn på toppen av filen
|
||
|
|
val webClientId = localProperties.getProperty("WEB_CLIENT_ID") ?: ""
|
||
|
|
|
||
|
|
// Opprett BuildConfig-feltet.
|
||
|
|
// Vi legger på ekstra hermetegn (\") for at det skal bli en String i Java-koden.
|
||
|
|
buildConfigField("String", "WEB_CLIENT_ID", "\"$webClientId\"")
|
||
|
|
}
|
||
|
|
|
||
|
|
// NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden
|
||
|
|
buildFeatures {
|
||
|
|
buildConfig = true
|
||
|
|
}
|
||
|
|
|
||
|
|
buildTypes {
|
||
|
|
release {
|
||
|
|
isMinifyEnabled = false
|
||
|
|
proguardFiles(
|
||
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||
|
|
"proguard-rules.pro"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
compileOptions {
|
||
|
|
sourceCompatibility = JavaVersion.VERSION_11
|
||
|
|
targetCompatibility = JavaVersion.VERSION_11
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
dependencies {
|
||
|
|
implementation(libs.appcompat)
|
||
|
|
implementation(libs.material)
|
||
|
|
implementation(libs.activity)
|
||
|
|
implementation(libs.constraintlayout)
|
||
|
|
testImplementation(libs.junit)
|
||
|
|
androidTestImplementation(libs.ext.junit)
|
||
|
|
androidTestImplementation(libs.espresso.core)
|
||
|
|
|
||
|
|
// Nettverk og JSON-håndtering
|
||
|
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||
|
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||
|
|
implementation("com.google.code.gson:gson:2.10.1")
|
||
|
|
|
||
|
|
// Navigation Component
|
||
|
|
val navVersion = "2.8.5"
|
||
|
|
implementation("androidx.navigation:navigation-fragment:$navVersion")
|
||
|
|
implementation("androidx.navigation:navigation-ui:$navVersion")
|
||
|
|
|
||
|
|
implementation("com.google.android.gms:play-services-auth:20.7.0")
|
||
|
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||
|
|
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||
|
|
|
||
|
|
implementation("androidx.work:work-runtime:2.9.0")
|
||
|
|
|
||
|
|
// Swipe Refresh Layout
|
||
|
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||
|
|
|
||
|
|
// NY LINJE: Firebase BOM (Bill of Materials) styrer versjoner
|
||
|
|
implementation(platform("com.google.firebase:firebase-bom:33.1.2"))
|
||
|
|
|
||
|
|
// NY LINJE: (Valgfritt, men lurt for statistikk)
|
||
|
|
implementation("com.google.firebase:firebase-analytics")
|
||
|
|
|
||
|
|
// NYTT: Firebase Cloud Messaging lagt til her
|
||
|
|
implementation("com.google.firebase:firebase-messaging")
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\proguard-rules.pro
|
||
|
|
============================================================
|
||
|
|
# Add project specific ProGuard rules here.
|
||
|
|
# You can control the set of applied configuration files using the
|
||
|
|
# proguardFiles setting in build.gradle.
|
||
|
|
#
|
||
|
|
# For more details, see
|
||
|
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||
|
|
|
||
|
|
# If your project uses WebView with JS, uncomment the following
|
||
|
|
# and specify the fully qualified class name to the JavaScript interface
|
||
|
|
# class:
|
||
|
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||
|
|
# public *;
|
||
|
|
#}
|
||
|
|
|
||
|
|
# Uncomment this to preserve the line number information for
|
||
|
|
# debugging stack traces.
|
||
|
|
#-keepattributes SourceFile,LineNumberTable
|
||
|
|
|
||
|
|
# If you keep the line number information, uncomment this to
|
||
|
|
# hide the original source file name.
|
||
|
|
#-renamesourcefileattribute SourceFile
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\androidTest\java\com\kbs\kbsintranett\ExampleInstrumentedTest.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.content.Context;
|
||
|
|
|
||
|
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||
|
|
|
||
|
|
import org.junit.Test;
|
||
|
|
import org.junit.runner.RunWith;
|
||
|
|
|
||
|
|
import static org.junit.Assert.*;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Instrumented test, which will execute on an Android device.
|
||
|
|
*
|
||
|
|
* @see <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\AddTaskBottomSheet.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.app.AlertDialog;
|
||
|
|
import android.app.DatePickerDialog;
|
||
|
|
import android.app.Dialog;
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.view.WindowManager;
|
||
|
|
import android.widget.Button;
|
||
|
|
import android.widget.EditText;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import android.widget.Toast;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||
|
|
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||
|
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.Calendar;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
import java.util.Map;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class AddTaskBottomSheet extends BottomSheetDialogFragment {
|
||
|
|
|
||
|
|
private EditText etTitle, etDesc;
|
||
|
|
private Button btnDate, btnUsers, btnSave, btnClearDate; // NYTT
|
||
|
|
private TextView txtSheetTitle, txtDatePreview, txtUsersPreview;
|
||
|
|
|
||
|
|
private Calendar dueDate = Calendar.getInstance();
|
||
|
|
private boolean hasDate = true; // NYTT
|
||
|
|
|
||
|
|
private List<User> filteredUsers = new ArrayList<>();
|
||
|
|
private List<User> selectedUsers = new ArrayList<>();
|
||
|
|
private TaskItem taskToEdit = null;
|
||
|
|
|
||
|
|
public interface OnTaskAddedListener {
|
||
|
|
void onTaskAdded(TaskItem task);
|
||
|
|
void onTaskUpdated(TaskItem task);
|
||
|
|
}
|
||
|
|
|
||
|
|
private OnTaskAddedListener listener;
|
||
|
|
|
||
|
|
public void setOnTaskAddedListener(OnTaskAddedListener listener) {
|
||
|
|
this.listener = listener;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void setTaskToEdit(TaskItem task) {
|
||
|
|
this.taskToEdit = task;
|
||
|
|
}
|
||
|
|
|
||
|
|
@NonNull
|
||
|
|
@Override
|
||
|
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||
|
|
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
|
||
|
|
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||
|
|
dialog.setOnShowListener(dialogInterface -> {
|
||
|
|
BottomSheetDialog d = (BottomSheetDialog) dialogInterface;
|
||
|
|
View bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet);
|
||
|
|
if (bottomSheet != null) {
|
||
|
|
BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return dialog;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
View v = inflater.inflate(R.layout.bottom_sheet_add_task, container, false);
|
||
|
|
|
||
|
|
txtSheetTitle = v.findViewById(R.id.txt_sheet_title);
|
||
|
|
etTitle = v.findViewById(R.id.et_task_title);
|
||
|
|
etDesc = v.findViewById(R.id.et_task_desc);
|
||
|
|
btnDate = v.findViewById(R.id.btn_task_date);
|
||
|
|
btnClearDate = v.findViewById(R.id.btn_clear_date); // NYTT
|
||
|
|
btnUsers = v.findViewById(R.id.btn_task_users);
|
||
|
|
btnSave = v.findViewById(R.id.btn_save_task);
|
||
|
|
txtDatePreview = v.findViewById(R.id.txt_date_preview);
|
||
|
|
txtUsersPreview = v.findViewById(R.id.txt_users_preview);
|
||
|
|
|
||
|
|
if (taskToEdit != null) {
|
||
|
|
txtSheetTitle.setText("Rediger Oppgave");
|
||
|
|
etTitle.setText(taskToEdit.getTitle());
|
||
|
|
etDesc.setText(taskToEdit.getDescription());
|
||
|
|
|
||
|
|
if (taskToEdit.getDueDate() > 0) {
|
||
|
|
dueDate.setTimeInMillis(taskToEdit.getDueDate());
|
||
|
|
hasDate = true;
|
||
|
|
} else {
|
||
|
|
hasDate = false;
|
||
|
|
}
|
||
|
|
btnSave.setText("Oppdater Oppgave");
|
||
|
|
} else {
|
||
|
|
dueDate.add(Calendar.DAY_OF_MONTH, 1);
|
||
|
|
hasDate = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
updateDatePreview();
|
||
|
|
|
||
|
|
btnDate.setOnClickListener(view -> {
|
||
|
|
new DatePickerDialog(getContext(), (d, y, m, day) -> {
|
||
|
|
dueDate.set(y, m, day);
|
||
|
|
hasDate = true;
|
||
|
|
updateDatePreview();
|
||
|
|
}, dueDate.get(Calendar.YEAR), dueDate.get(Calendar.MONTH), dueDate.get(Calendar.DAY_OF_MONTH)).show();
|
||
|
|
});
|
||
|
|
|
||
|
|
// NYTT: Knapp for å fjerne frist
|
||
|
|
btnClearDate.setOnClickListener(v1 -> {
|
||
|
|
hasDate = false;
|
||
|
|
updateDatePreview();
|
||
|
|
});
|
||
|
|
|
||
|
|
btnUsers.setOnClickListener(view -> showUserSelectionDialog());
|
||
|
|
btnSave.setOnClickListener(view -> saveTask());
|
||
|
|
|
||
|
|
fetchAndFilterUsers();
|
||
|
|
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchAndFilterUsers() {
|
||
|
|
RetrofitClient.getApiService().getUsersList().enqueue(new Callback<List<User>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
filteredUsers = UserFilterHelper.getFilteredUsers(response.body());
|
||
|
|
|
||
|
|
if (taskToEdit != null) {
|
||
|
|
selectedUsers.clear();
|
||
|
|
Map<String, Boolean> currentAssignees = taskToEdit.getAssigneeStatus();
|
||
|
|
for (User u : filteredUsers) {
|
||
|
|
if (currentAssignees.containsKey(u.getEmail())) {
|
||
|
|
selectedUsers.add(u);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
updateUsersPreview();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<User>> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showUserSelectionDialog() {
|
||
|
|
if (filteredUsers.isEmpty()) {
|
||
|
|
Toast.makeText(getContext(), "Henter tilgjengelige personer...", Toast.LENGTH_SHORT).show();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
String[] names = new String[filteredUsers.size()];
|
||
|
|
boolean[] checked = new boolean[filteredUsers.size()];
|
||
|
|
for (int i = 0; i < filteredUsers.size(); i++) {
|
||
|
|
names[i] = filteredUsers.get(i).getName();
|
||
|
|
boolean isSelected = false;
|
||
|
|
for(User su : selectedUsers) {
|
||
|
|
if(su.getEmail().equalsIgnoreCase(filteredUsers.get(i).getEmail())) {
|
||
|
|
isSelected = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
checked[i] = isSelected;
|
||
|
|
}
|
||
|
|
new AlertDialog.Builder(getContext())
|
||
|
|
.setTitle("Tildel til...")
|
||
|
|
.setMultiChoiceItems(names, checked, (dialog, which, isChecked) -> {
|
||
|
|
User user = filteredUsers.get(which);
|
||
|
|
if (isChecked) {
|
||
|
|
selectedUsers.add(user);
|
||
|
|
} else {
|
||
|
|
selectedUsers.removeIf(u -> u.getEmail().equalsIgnoreCase(user.getEmail()));
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.setPositiveButton("OK", (dialog, which) -> updateUsersPreview())
|
||
|
|
.show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateUsersPreview() {
|
||
|
|
if (selectedUsers.isEmpty()) txtUsersPreview.setText("Kun meg");
|
||
|
|
else if (selectedUsers.size() == 1) txtUsersPreview.setText(selectedUsers.get(0).getName());
|
||
|
|
else txtUsersPreview.setText(selectedUsers.size() + " personer valgt");
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateDatePreview() {
|
||
|
|
if (hasDate) {
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault());
|
||
|
|
txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime()));
|
||
|
|
btnClearDate.setVisibility(View.VISIBLE);
|
||
|
|
} else {
|
||
|
|
txtDatePreview.setText("Ingen frist");
|
||
|
|
btnClearDate.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void saveTask() {
|
||
|
|
String title = etTitle.getText().toString().trim();
|
||
|
|
if (title.isEmpty()) {
|
||
|
|
etTitle.setError("Mangler tittel");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
long finalDueDate = hasDate ? dueDate.getTimeInMillis() : 0;
|
||
|
|
|
||
|
|
if (taskToEdit != null) {
|
||
|
|
Map<String, Boolean> oldStatus = taskToEdit.getAssigneeStatus();
|
||
|
|
taskToEdit.getAssigneeStatus().clear();
|
||
|
|
|
||
|
|
if (selectedUsers.isEmpty()) {
|
||
|
|
String myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
taskToEdit.addAssignee(myEmail);
|
||
|
|
if (oldStatus.containsKey(myEmail)) taskToEdit.setParticipantStatus(myEmail, oldStatus.get(myEmail));
|
||
|
|
} else {
|
||
|
|
for (User u : selectedUsers) {
|
||
|
|
taskToEdit.addAssignee(u.getEmail());
|
||
|
|
if (oldStatus.containsKey(u.getEmail())) {
|
||
|
|
taskToEdit.setParticipantStatus(u.getEmail(), oldStatus.get(u.getEmail()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
taskToEdit.setTitle(title);
|
||
|
|
taskToEdit.setDescription(etDesc.getText().toString());
|
||
|
|
taskToEdit.setDueDate(finalDueDate);
|
||
|
|
if (listener != null) listener.onTaskUpdated(taskToEdit);
|
||
|
|
} else {
|
||
|
|
TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), finalDueDate);
|
||
|
|
if (selectedUsers.isEmpty()) {
|
||
|
|
newTask.addAssignee(UserManager.getInstance().getUserEmail());
|
||
|
|
} else {
|
||
|
|
for (User u : selectedUsers) newTask.addAssignee(u.getEmail());
|
||
|
|
}
|
||
|
|
if (listener != null) listener.onTaskAdded(newTask);
|
||
|
|
}
|
||
|
|
dismiss();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.Manifest;
|
||
|
|
import android.app.NotificationChannel;
|
||
|
|
import android.app.NotificationManager;
|
||
|
|
import android.app.PendingIntent;
|
||
|
|
import android.content.BroadcastReceiver;
|
||
|
|
import android.content.Context;
|
||
|
|
import android.content.Intent;
|
||
|
|
import android.content.pm.PackageManager;
|
||
|
|
import android.os.Build;
|
||
|
|
import androidx.core.app.ActivityCompat;
|
||
|
|
import androidx.core.app.NotificationCompat;
|
||
|
|
import androidx.core.app.NotificationManagerCompat;
|
||
|
|
import androidx.core.content.ContextCompat; // <-- Denne manglet
|
||
|
|
|
||
|
|
public class AlarmReceiver extends BroadcastReceiver {
|
||
|
|
|
||
|
|
private static final String CHANNEL_ID = "kbs_calendar_channel";
|
||
|
|
private static final String CHANNEL_NAME = "KBS Kalendervarsler";
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onReceive(Context context, Intent intent) {
|
||
|
|
String title = intent.getStringExtra("TITLE");
|
||
|
|
String message = intent.getStringExtra("MESSAGE");
|
||
|
|
int notificationId = intent.getIntExtra("ID", 0);
|
||
|
|
|
||
|
|
createNotificationChannel(context);
|
||
|
|
showNotification(context, title, message, notificationId);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showNotification(Context context, String title, String message, int notificationId) {
|
||
|
|
// Sjekk rettigheter for Android 13+
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
|
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||
|
|
// Vi kan ikke vise varsel uten rettighet.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Intent for hva som skjer når man trykker på varselet (åpne appen)
|
||
|
|
Intent tapIntent = new Intent(context, MainActivity.class);
|
||
|
|
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||
|
|
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||
|
|
context,
|
||
|
|
0,
|
||
|
|
tapIntent,
|
||
|
|
PendingIntent.FLAG_IMMUTABLE
|
||
|
|
);
|
||
|
|
|
||
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||
|
|
.setSmallIcon(R.drawable.ic_stat_kbs)
|
||
|
|
.setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue)) // Setter KBS-blå farge på ikonet
|
||
|
|
.setContentTitle(title)
|
||
|
|
.setContentText(message)
|
||
|
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||
|
|
.setContentIntent(pendingIntent)
|
||
|
|
.setAutoCancel(true);
|
||
|
|
|
||
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||
|
|
notificationManager.notify(notificationId, builder.build());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void createNotificationChannel(Context context) {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
|
int importance = NotificationManager.IMPORTANCE_HIGH;
|
||
|
|
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
|
||
|
|
channel.setDescription("Varsler for kalenderhendelser i KBS Intranett");
|
||
|
|
|
||
|
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||
|
|
if (notificationManager != null) {
|
||
|
|
notificationManager.createNotificationChannel(channel);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmScheduler.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.app.AlarmManager;
|
||
|
|
import android.app.PendingIntent;
|
||
|
|
import android.content.Context;
|
||
|
|
import android.content.Intent;
|
||
|
|
import android.content.SharedPreferences;
|
||
|
|
import android.os.Build;
|
||
|
|
import android.util.Log;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
|
||
|
|
public class AlarmScheduler {
|
||
|
|
|
||
|
|
private static final String TAG = "AlarmScheduler";
|
||
|
|
private static final String PREFS_NAME = "kbs_alarm_history";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Denne metoden går gjennom en liste hendelser og setter alarmer for dem.
|
||
|
|
*/
|
||
|
|
public static void scheduleAlarmsForEvents(Context context, List<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.content.SharedPreferences; // NYTT
|
||
|
|
import android.util.Log;
|
||
|
|
import com.google.gson.Gson;
|
||
|
|
import com.google.gson.reflect.TypeToken;
|
||
|
|
import java.io.BufferedReader;
|
||
|
|
import java.io.File;
|
||
|
|
import java.io.FileInputStream;
|
||
|
|
import java.io.FileOutputStream;
|
||
|
|
import java.io.InputStreamReader;
|
||
|
|
import java.lang.reflect.Type;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class CacheManager {
|
||
|
|
private static final String FILE_CALENDAR = "cache_calendar.json";
|
||
|
|
private static final String FILE_NEWS = "cache_news.json";
|
||
|
|
private static final String FILE_HANDBOOK = "cache_handbook.json";
|
||
|
|
private static final String FILE_TASKS = "cache_tasks.json";
|
||
|
|
|
||
|
|
private static final String TAG = "CacheManager";
|
||
|
|
private static final Gson gson = new Gson();
|
||
|
|
|
||
|
|
// NYTT: SharedPreferences for tidsstempler
|
||
|
|
private static final String PREFS_CACHE = "kbs_cache_prefs";
|
||
|
|
|
||
|
|
// --- KALENDER ---
|
||
|
|
public static void saveCalendarEvents(Context context, List<CalendarEvent> events) {
|
||
|
|
saveList(context, FILE_CALENDAR, events);
|
||
|
|
setLastFetchTime(context, "calendar"); // NYTT
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
setLastFetchTime(context, "news"); // NYTT
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
// Håndboken endres sjelden, trenger kanskje ikke tidssjekk, men greit å ha
|
||
|
|
}
|
||
|
|
|
||
|
|
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<>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- OPPGAVER ---
|
||
|
|
public static void saveTasks(Context context, List<TaskItem> tasks) {
|
||
|
|
saveList(context, FILE_TASKS, tasks);
|
||
|
|
setLastFetchTime(context, "tasks"); // NYTT
|
||
|
|
}
|
||
|
|
|
||
|
|
public static List<TaskItem> getTasks(Context context) {
|
||
|
|
Type type = new TypeToken<List<TaskItem>>() {}.getType();
|
||
|
|
List<TaskItem> list = loadList(context, FILE_TASKS, type);
|
||
|
|
return list != null ? list : new ArrayList<>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- LOGIKK FOR TID ---
|
||
|
|
|
||
|
|
private static void setLastFetchTime(Context context, String key) {
|
||
|
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
|
||
|
|
prefs.edit().putLong(key + "_timestamp", System.currentTimeMillis()).apply();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sjekker om cachen er gyldig.
|
||
|
|
* @param maxAgeMinutes Hvor gammel cachen kan være før vi henter på nytt (f.eks. 60 minutter).
|
||
|
|
* Hvis push fungerer, kan vi sette denne høyt!
|
||
|
|
*/
|
||
|
|
public static boolean isCacheValid(Context context, String key, int maxAgeMinutes) {
|
||
|
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
|
||
|
|
long lastTime = prefs.getLong(key + "_timestamp", 0);
|
||
|
|
long diff = System.currentTimeMillis() - lastTime;
|
||
|
|
// Konverter minutter til millisekunder
|
||
|
|
long maxDiff = maxAgeMinutes * 60 * 1000L;
|
||
|
|
|
||
|
|
return diff < maxDiff;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Metode for å tvinge oppdatering neste gang (brukes av FCM)
|
||
|
|
public static void invalidateCache(Context context, String key) {
|
||
|
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
|
||
|
|
prefs.edit().remove(key + "_timestamp").apply();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- GENERISKE HJELPEMETODER (Uendret) ---
|
||
|
|
|
||
|
|
private static <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();
|
||
|
|
} 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<RecyclerView.ViewHolder> {
|
||
|
|
|
||
|
|
public static final int TYPE_EVENT = 0;
|
||
|
|
public static final int TYPE_YEAR_HEADER = 1;
|
||
|
|
|
||
|
|
private List<Object> items;
|
||
|
|
private final OnItemClickListener listener;
|
||
|
|
|
||
|
|
public interface OnItemClickListener {
|
||
|
|
void onItemClick(CalendarEvent event);
|
||
|
|
}
|
||
|
|
|
||
|
|
public CalendarAdapter(List<Object> items, OnItemClickListener listener) {
|
||
|
|
this.items = items;
|
||
|
|
this.listener = listener;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int getItemViewType(int position) {
|
||
|
|
return (items.get(position) instanceof String) ? TYPE_YEAR_HEADER : TYPE_EVENT;
|
||
|
|
}
|
||
|
|
|
||
|
|
@NonNull
|
||
|
|
@Override
|
||
|
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||
|
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||
|
|
if (viewType == TYPE_YEAR_HEADER) {
|
||
|
|
return new YearViewHolder(inflater.inflate(R.layout.item_calendar_year_header, parent, false));
|
||
|
|
} else {
|
||
|
|
return new EventViewHolder(inflater.inflate(R.layout.item_calendar, parent, false));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||
|
|
Object item = items.get(position);
|
||
|
|
|
||
|
|
if (holder instanceof YearViewHolder) {
|
||
|
|
((YearViewHolder) holder).yearText.setText((String) item);
|
||
|
|
}
|
||
|
|
else if (holder instanceof EventViewHolder) {
|
||
|
|
CalendarEvent event = (CalendarEvent) item;
|
||
|
|
EventViewHolder vh = (EventViewHolder) holder;
|
||
|
|
|
||
|
|
vh.day.setText(event.getDay());
|
||
|
|
vh.month.setText(event.getMonth());
|
||
|
|
vh.time.setText(event.getTime());
|
||
|
|
vh.title.setText(event.getTitle());
|
||
|
|
|
||
|
|
// --- ÅRSTALL LOGIKK: BAKGRUNNSFARGE ---
|
||
|
|
// Vi henter årstall fra datoen (yyyy-MM-dd)
|
||
|
|
String year = "2025";
|
||
|
|
if (event.getRawDate() != null && event.getRawDate().length() >= 4) {
|
||
|
|
year = event.getRawDate().substring(0, 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alternerende bakgrunn basert på år (partall vs oddetall)
|
||
|
|
int yearInt = Integer.parseInt(year);
|
||
|
|
if (yearInt % 2 == 0) {
|
||
|
|
vh.itemView.setBackgroundColor(Color.parseColor("#F5F7FA")); // KBS Very Light Blue
|
||
|
|
} else {
|
||
|
|
vh.itemView.setBackgroundColor(Color.WHITE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Privat-markering og farge på datoboks
|
||
|
|
boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:");
|
||
|
|
try {
|
||
|
|
int color = Color.parseColor(isPrivate ? "#673AB7" : event.getCalendarColor());
|
||
|
|
vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(color));
|
||
|
|
} catch (Exception e) {
|
||
|
|
vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#0069B3")));
|
||
|
|
}
|
||
|
|
|
||
|
|
vh.itemView.setOnClickListener(v -> listener.onItemClick(event));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int getItemCount() {
|
||
|
|
return items.size();
|
||
|
|
}
|
||
|
|
|
||
|
|
static class EventViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView day, month, title, time;
|
||
|
|
LinearLayout dateBox;
|
||
|
|
EventViewHolder(View view) {
|
||
|
|
super(view);
|
||
|
|
day = view.findViewById(R.id.cal_day);
|
||
|
|
month = view.findViewById(R.id.cal_month);
|
||
|
|
title = view.findViewById(R.id.cal_title);
|
||
|
|
time = view.findViewById(R.id.cal_time);
|
||
|
|
dateBox = view.findViewById(R.id.date_box_background);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static class YearViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView yearText;
|
||
|
|
YearViewHolder(View view) {
|
||
|
|
super(view);
|
||
|
|
yearText = view.findViewById(R.id.txt_calendar_year);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarDetailsBottomSheet.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.app.AlertDialog;
|
||
|
|
import android.content.Intent;
|
||
|
|
import android.content.res.ColorStateList;
|
||
|
|
import android.graphics.Color;
|
||
|
|
import android.net.Uri;
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.text.Html;
|
||
|
|
import android.text.method.LinkMovementMethod;
|
||
|
|
import android.text.util.Linkify;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.Button;
|
||
|
|
import android.widget.LinearLayout;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import android.widget.Toast;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.navigation.fragment.NavHostFragment;
|
||
|
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||
|
|
import com.google.gson.JsonElement;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.regex.Matcher;
|
||
|
|
import java.util.regex.Pattern;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment {
|
||
|
|
private CalendarEvent event;
|
||
|
|
private OnEventChangeListener changeListener;
|
||
|
|
|
||
|
|
private static final String PRIVATE_EVENT_COLOR = "#673AB7";
|
||
|
|
|
||
|
|
public interface OnEventChangeListener {
|
||
|
|
void onEventChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
public CalendarDetailsBottomSheet(CalendarEvent event) {
|
||
|
|
this.event = event;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void setOnEventChangeListener(OnEventChangeListener listener) {
|
||
|
|
this.changeListener = listener;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
View view = inflater.inflate(R.layout.bottom_sheet_calendar_details, container, false);
|
||
|
|
|
||
|
|
TextView title = view.findViewById(R.id.sheet_title);
|
||
|
|
TextView time = view.findViewById(R.id.sheet_time);
|
||
|
|
TextView desc = view.findViewById(R.id.sheet_desc);
|
||
|
|
TextView loc = view.findViewById(R.id.sheet_location);
|
||
|
|
TextView calName = view.findViewById(R.id.sheet_calendar_name);
|
||
|
|
TextView participantsView = view.findViewById(R.id.sheet_participants);
|
||
|
|
TextView organizerView = view.findViewById(R.id.sheet_organizer); // NYTT
|
||
|
|
|
||
|
|
LinearLayout adminLayout = view.findViewById(R.id.layout_admin_buttons);
|
||
|
|
Button btnDelete = view.findViewById(R.id.btn_delete);
|
||
|
|
Button btnEdit = view.findViewById(R.id.btn_edit);
|
||
|
|
|
||
|
|
title.setText(event.getTitle());
|
||
|
|
time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")");
|
||
|
|
|
||
|
|
// Sjekk om privat
|
||
|
|
boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:");
|
||
|
|
|
||
|
|
if (isPrivate) {
|
||
|
|
calName.setText(event.getCalendarName().toUpperCase() + " (BEGRENSET INNSYN)");
|
||
|
|
try {
|
||
|
|
calName.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor(PRIVATE_EVENT_COLOR)));
|
||
|
|
} catch (Exception e) {}
|
||
|
|
|
||
|
|
showParticipants(event.getDescription(), participantsView);
|
||
|
|
|
||
|
|
} else {
|
||
|
|
calName.setText(event.getCalendarName().toUpperCase());
|
||
|
|
try {
|
||
|
|
int color = Color.parseColor(event.getCalendarColor());
|
||
|
|
calName.setBackgroundTintList(ColorStateList.valueOf(color));
|
||
|
|
} catch (Exception e) {}
|
||
|
|
|
||
|
|
participantsView.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// VIS ARRANGØR ALLTID
|
||
|
|
showOrganizer(event.getDescription(), organizerView);
|
||
|
|
|
||
|
|
// --- BESKRIVELSE OG LENKER ---
|
||
|
|
if (!event.getDescription().isEmpty()) {
|
||
|
|
String cleanDesc = event.getDescription()
|
||
|
|
.replaceAll("#varsel:[\\d,]+", "")
|
||
|
|
.replaceAll("#deltakere:[^\\s]+", "")
|
||
|
|
.replaceAll("#arrangor:.+", "") // Fjern arrangør fra brødtekst
|
||
|
|
.trim();
|
||
|
|
|
||
|
|
if (!cleanDesc.isEmpty()) {
|
||
|
|
desc.setText(Html.fromHtml(cleanDesc, Html.FROM_HTML_MODE_COMPACT));
|
||
|
|
Linkify.addLinks(desc, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS);
|
||
|
|
desc.setMovementMethod(LinkMovementMethod.getInstance());
|
||
|
|
desc.setVisibility(View.VISIBLE);
|
||
|
|
} else {
|
||
|
|
desc.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
desc.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- ADRESSE OG KART ---
|
||
|
|
if (!event.getLocation().isEmpty()) {
|
||
|
|
loc.setText(event.getLocation());
|
||
|
|
loc.setVisibility(View.VISIBLE);
|
||
|
|
loc.setOnClickListener(v -> {
|
||
|
|
String location = event.getLocation();
|
||
|
|
Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + Uri.encode(location));
|
||
|
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
||
|
|
mapIntent.setPackage("com.google.android.apps.maps");
|
||
|
|
try {
|
||
|
|
startActivity(mapIntent);
|
||
|
|
} catch (Exception e) {
|
||
|
|
mapIntent.setPackage(null);
|
||
|
|
try {
|
||
|
|
startActivity(mapIntent);
|
||
|
|
} catch (Exception ex) {
|
||
|
|
Toast.makeText(getContext(), "Fant ingen kart-app", Toast.LENGTH_SHORT).show();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
loc.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sjekk admin-rettigheter
|
||
|
|
if (UserManager.getInstance().isEditorOrAbove()) {
|
||
|
|
boolean canEdit = UserManager.getInstance().getWriteableCalendars().contains(event.getCalendarName())
|
||
|
|
|| UserManager.getInstance().isAdmin();
|
||
|
|
|
||
|
|
if (canEdit) {
|
||
|
|
adminLayout.setVisibility(View.VISIBLE);
|
||
|
|
btnDelete.setOnClickListener(v -> confirmDelete());
|
||
|
|
btnEdit.setOnClickListener(v -> {
|
||
|
|
Bundle bundle = new Bundle();
|
||
|
|
bundle.putSerializable("edit_event", event);
|
||
|
|
NavHostFragment.findNavController(this).navigate(R.id.navigation_create_event, bundle);
|
||
|
|
dismiss();
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
adminLayout.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showParticipants(String rawDescription, TextView view) {
|
||
|
|
if (rawDescription == null) return;
|
||
|
|
Matcher m = Pattern.compile("#deltakere:([^\\s]+)").matcher(rawDescription);
|
||
|
|
if (m.find()) {
|
||
|
|
String allEmails = m.group(1);
|
||
|
|
String[] emails = allEmails.split(",");
|
||
|
|
StringBuilder sb = new StringBuilder();
|
||
|
|
sb.append("<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.ProgressBar;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.fragment.app.Fragment;
|
||
|
|
import androidx.navigation.Navigation;
|
||
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.List;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class CalendarFullFragment extends Fragment {
|
||
|
|
|
||
|
|
private RecyclerView recyclerView;
|
||
|
|
private ProgressBar progressBar;
|
||
|
|
private TextView emptyView;
|
||
|
|
private FloatingActionButton fabAddEvent;
|
||
|
|
private LinearLayoutManager layoutManager;
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
return inflater.inflate(R.layout.fragment_calendar_full, container, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||
|
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
recyclerView = view.findViewById(R.id.recycler_full_calendar);
|
||
|
|
progressBar = view.findViewById(R.id.loading_full_calendar);
|
||
|
|
emptyView = view.findViewById(R.id.empty_view_calendar);
|
||
|
|
fabAddEvent = view.findViewById(R.id.fab_add_calendar_event);
|
||
|
|
|
||
|
|
layoutManager = new LinearLayoutManager(getContext());
|
||
|
|
recyclerView.setLayoutManager(layoutManager);
|
||
|
|
|
||
|
|
if (!UserManager.getInstance().getWriteableCalendars().isEmpty() || UserManager.getInstance().isEditorOrAbove()) {
|
||
|
|
fabAddEvent.setVisibility(View.VISIBLE);
|
||
|
|
fabAddEvent.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_create_event));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onResume() {
|
||
|
|
super.onResume();
|
||
|
|
fetchAllEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchAllEvents() {
|
||
|
|
progressBar.setVisibility(View.VISIBLE);
|
||
|
|
new Thread(() -> {
|
||
|
|
List<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);
|
||
|
|
displaySortedList(allEvents);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
|
||
|
|
if (!isAdded()) return;
|
||
|
|
progressBar.setVisibility(View.GONE);
|
||
|
|
displaySortedList(deviceEvents);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void displaySortedList(List<CalendarEvent> events) {
|
||
|
|
if (events.isEmpty()) {
|
||
|
|
emptyView.setVisibility(View.VISIBLE);
|
||
|
|
recyclerView.setVisibility(View.GONE);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
emptyView.setVisibility(View.GONE);
|
||
|
|
recyclerView.setVisibility(View.VISIBLE);
|
||
|
|
|
||
|
|
List<Object> itemsWithHeaders = new ArrayList<>();
|
||
|
|
String currentYear = "";
|
||
|
|
|
||
|
|
for (CalendarEvent e : events) {
|
||
|
|
String year = "Ukjent år";
|
||
|
|
if (e.getRawDate() != null && e.getRawDate().length() >= 4) {
|
||
|
|
year = e.getRawDate().substring(0, 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!year.equals(currentYear)) {
|
||
|
|
itemsWithHeaders.add(year);
|
||
|
|
currentYear = year;
|
||
|
|
}
|
||
|
|
itemsWithHeaders.add(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
CalendarAdapter adapter = new CalendarAdapter(itemsWithHeaders, event -> {
|
||
|
|
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
|
||
|
|
sheet.setOnEventChangeListener(CalendarFullFragment.this::fetchAllEvents);
|
||
|
|
sheet.show(getParentFragmentManager(), "CalendarDetails");
|
||
|
|
});
|
||
|
|
recyclerView.setAdapter(adapter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarManager.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.content.Context;
|
||
|
|
import android.content.pm.PackageManager;
|
||
|
|
import android.database.Cursor;
|
||
|
|
import android.provider.CalendarContract;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.Collections;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.HashSet;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
import java.util.Set;
|
||
|
|
import java.util.TimeZone;
|
||
|
|
|
||
|
|
public class CalendarManager {
|
||
|
|
private static List<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.Date;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
import java.util.regex.Matcher;
|
||
|
|
import java.util.regex.Pattern;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class CreateEventFragment extends Fragment {
|
||
|
|
private EditText etTitle, etDesc, etLocation;
|
||
|
|
private Spinner spinnerCalendar, spinnerRecurrence;
|
||
|
|
private ChipGroup chipGroupReminders;
|
||
|
|
private Switch switchAllDay;
|
||
|
|
private TextView txtPreview, txtSelectedParticipants;
|
||
|
|
private Button btnStartDate, btnStartTime, btnEndDate, btnEndTime, btnSave, btnSelectParticipants;
|
||
|
|
private RadioButton rbAll, rbSpecific;
|
||
|
|
|
||
|
|
private Calendar startCal = Calendar.getInstance();
|
||
|
|
private Calendar endCal = Calendar.getInstance();
|
||
|
|
|
||
|
|
private String selectedRRule = null;
|
||
|
|
private boolean isCustomRecurrence = false;
|
||
|
|
|
||
|
|
// BRUKERLISTER
|
||
|
|
private List<User> filteredUsers = new ArrayList<>();
|
||
|
|
|
||
|
|
private String originalOrganizer = null;
|
||
|
|
private CalendarEvent eventToEdit = null;
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
return inflater.inflate(R.layout.fragment_create_event, container, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||
|
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
|
||
|
|
// Initialisering av Views
|
||
|
|
etTitle = view.findViewById(R.id.et_title);
|
||
|
|
etDesc = view.findViewById(R.id.et_desc);
|
||
|
|
etLocation = view.findViewById(R.id.et_location);
|
||
|
|
switchAllDay = view.findViewById(R.id.switch_all_day);
|
||
|
|
spinnerCalendar = view.findViewById(R.id.spinner_calendar);
|
||
|
|
spinnerRecurrence = view.findViewById(R.id.spinner_recurrence);
|
||
|
|
chipGroupReminders = view.findViewById(R.id.chip_group_reminders);
|
||
|
|
txtPreview = view.findViewById(R.id.txt_time_preview);
|
||
|
|
txtSelectedParticipants = view.findViewById(R.id.txt_selected_participants);
|
||
|
|
btnStartDate = view.findViewById(R.id.btn_start_date);
|
||
|
|
btnStartTime = view.findViewById(R.id.btn_start_time);
|
||
|
|
btnEndDate = view.findViewById(R.id.btn_end_date);
|
||
|
|
btnEndTime = view.findViewById(R.id.btn_end_time);
|
||
|
|
btnSave = view.findViewById(R.id.btn_save_event);
|
||
|
|
btnSelectParticipants = view.findViewById(R.id.btn_select_participants);
|
||
|
|
rbAll = view.findViewById(R.id.rb_visibility_all);
|
||
|
|
rbSpecific = view.findViewById(R.id.rb_visibility_specific);
|
||
|
|
|
||
|
|
// Standardtidspunkt (neste time)
|
||
|
|
startCal.add(Calendar.HOUR_OF_DAY, 1);
|
||
|
|
startCal.set(Calendar.MINUTE, 0);
|
||
|
|
endCal.setTime(startCal.getTime());
|
||
|
|
endCal.add(Calendar.HOUR_OF_DAY, 1);
|
||
|
|
|
||
|
|
setupCalendarSpinner();
|
||
|
|
setupReminderChips();
|
||
|
|
fetchUsers();
|
||
|
|
|
||
|
|
// Sjekk om vi redigerer en eksisterende hendelse
|
||
|
|
if (getArguments() != null && getArguments().containsKey("edit_event")) {
|
||
|
|
eventToEdit = (CalendarEvent) getArguments().getSerializable("edit_event");
|
||
|
|
prefillForm(eventToEdit);
|
||
|
|
btnSave.setText("Oppdater Hendelse");
|
||
|
|
}
|
||
|
|
|
||
|
|
updateRecurrenceSpinner();
|
||
|
|
updateUI();
|
||
|
|
|
||
|
|
// Listeners
|
||
|
|
switchAllDay.setOnCheckedChangeListener((btn, isChecked) -> updateUI());
|
||
|
|
btnStartDate.setOnClickListener(v -> pickDate(startCal, true));
|
||
|
|
btnEndDate.setOnClickListener(v -> pickDate(endCal, false));
|
||
|
|
btnStartTime.setOnClickListener(v -> pickTime(startCal));
|
||
|
|
btnEndTime.setOnClickListener(v -> pickTime(endCal));
|
||
|
|
btnSave.setOnClickListener(v -> submitEvent());
|
||
|
|
|
||
|
|
rbAll.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||
|
|
if (isChecked) {
|
||
|
|
btnSelectParticipants.setVisibility(View.GONE);
|
||
|
|
txtSelectedParticipants.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
rbSpecific.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||
|
|
if (isChecked) {
|
||
|
|
btnSelectParticipants.setVisibility(View.VISIBLE);
|
||
|
|
txtSelectedParticipants.setVisibility(View.VISIBLE);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
btnSelectParticipants.setOnClickListener(v -> showUserSelectionDialog());
|
||
|
|
|
||
|
|
spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||
|
|
@Override
|
||
|
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||
|
|
if (eventToEdit != null && position == 0 && selectedRRule != null) return;
|
||
|
|
String selected = parent.getItemAtPosition(position).toString();
|
||
|
|
if (selected.equals("Egendefinert...")) {
|
||
|
|
showCustomRecurrenceDialog();
|
||
|
|
} else if (selected.startsWith("Ikke gjenta")) {
|
||
|
|
selectedRRule = null;
|
||
|
|
} else {
|
||
|
|
generateStandardRRule(position);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override public void onNothingSelected(AdapterView<?> parent) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchUsers() {
|
||
|
|
RetrofitClient.getApiService().getUsersList().enqueue(new Callback<List<User>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
|
||
|
|
if (!isAdded()) return;
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
// BRUKER DEN SENTRALE HJELPEKLASSEN FOR FILTRERING
|
||
|
|
filteredUsers = UserFilterHelper.getFilteredUsers(response.body());
|
||
|
|
|
||
|
|
if (eventToEdit != null) {
|
||
|
|
parseParticipantsFromDescription(eventToEdit.getDescription());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<User>> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showUserSelectionDialog() {
|
||
|
|
if (filteredUsers.isEmpty()) {
|
||
|
|
Toast.makeText(getContext(), "Ingen personer funnet.", Toast.LENGTH_SHORT).show();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
String[] userNames = new String[filteredUsers.size()];
|
||
|
|
boolean[] checkedItems = new boolean[filteredUsers.size()];
|
||
|
|
|
||
|
|
for (int i = 0; i < filteredUsers.size(); i++) {
|
||
|
|
userNames[i] = filteredUsers.get(i).getName();
|
||
|
|
checkedItems[i] = filteredUsers.get(i).isSelected();
|
||
|
|
}
|
||
|
|
|
||
|
|
new AlertDialog.Builder(getContext())
|
||
|
|
.setTitle("Velg deltakere")
|
||
|
|
.setMultiChoiceItems(userNames, checkedItems, (dialog, which, isChecked) -> {
|
||
|
|
filteredUsers.get(which).setSelected(isChecked);
|
||
|
|
})
|
||
|
|
.setPositiveButton("OK", (dialog, which) -> updateParticipantPreview())
|
||
|
|
.setNegativeButton("Avbryt", null)
|
||
|
|
.show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateParticipantPreview() {
|
||
|
|
StringBuilder sb = new StringBuilder("Valgte: ");
|
||
|
|
int count = 0;
|
||
|
|
for (User u : filteredUsers) {
|
||
|
|
if (u.isSelected()) {
|
||
|
|
if (count > 0) sb.append(", ");
|
||
|
|
sb.append(u.getName());
|
||
|
|
count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (count == 0) txtSelectedParticipants.setText("Ingen valgt");
|
||
|
|
else txtSelectedParticipants.setText(sb.toString());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void parseParticipantsFromDescription(String desc) {
|
||
|
|
if (desc == null) return;
|
||
|
|
Pattern p = Pattern.compile("#deltakere:([^\\s]+)");
|
||
|
|
Matcher m = p.matcher(desc);
|
||
|
|
|
||
|
|
if (m.find()) {
|
||
|
|
rbSpecific.setChecked(true);
|
||
|
|
String[] emails = m.group(1).split(",");
|
||
|
|
for (String email : emails) {
|
||
|
|
for (User u : filteredUsers) {
|
||
|
|
if (u.getEmail().equalsIgnoreCase(email.trim())) {
|
||
|
|
u.setSelected(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
updateParticipantPreview();
|
||
|
|
} else {
|
||
|
|
rbAll.setChecked(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void prefillForm(CalendarEvent event) {
|
||
|
|
etTitle.setText(event.getTitle());
|
||
|
|
if (event.getDescription() != null) {
|
||
|
|
Matcher m = Pattern.compile("#arrangor:(.+)").matcher(event.getDescription());
|
||
|
|
if (m.find()) originalOrganizer = m.group(1).trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
String cleanDesc = event.getDescription()
|
||
|
|
.replaceAll("#varsel:[\\d,]+", "")
|
||
|
|
.replaceAll("#deltakere:[^\\s]+", "")
|
||
|
|
.replaceAll("#arrangor:.+", "")
|
||
|
|
.trim();
|
||
|
|
etDesc.setText(cleanDesc);
|
||
|
|
etLocation.setText(event.getLocation());
|
||
|
|
|
||
|
|
ArrayAdapter<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);
|
||
|
|
startCal.setTime(simpleFormat.parse(start));
|
||
|
|
if (event.getRawEndDate() != null) {
|
||
|
|
Calendar c = Calendar.getInstance();
|
||
|
|
c.setTime(simpleFormat.parse(event.getRawEndDate()));
|
||
|
|
c.add(Calendar.DAY_OF_MONTH, -1);
|
||
|
|
endCal.setTime(c.getTime());
|
||
|
|
} else {
|
||
|
|
endCal.setTime(startCal.getTime());
|
||
|
|
}
|
||
|
|
} else if (start.contains("T")) {
|
||
|
|
if (start.length() > 19) start = start.substring(0, 19);
|
||
|
|
startCal.setTime(isoFormat.parse(start));
|
||
|
|
String end = event.getRawEndDate();
|
||
|
|
if (end != null && end.contains("T")) {
|
||
|
|
if (end.length() > 19) end = end.substring(0, 19);
|
||
|
|
endCal.setTime(isoFormat.parse(end));
|
||
|
|
} else {
|
||
|
|
endCal.setTime(startCal.getTime());
|
||
|
|
endCal.add(Calendar.HOUR_OF_DAY, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
List<Integer> existingReminders = event.getReminders();
|
||
|
|
if (!existingReminders.isEmpty()) {
|
||
|
|
for (int i = 0; i < chipGroupReminders.getChildCount(); i++) {
|
||
|
|
Chip chip = (Chip) chipGroupReminders.getChildAt(i);
|
||
|
|
chip.setChecked(existingReminders.contains((Integer) chip.getTag()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (Exception e) {
|
||
|
|
Log.e("KBS_EDIT", "Feil ved prefill", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void setupCalendarSpinner() {
|
||
|
|
List<String> calendars = UserManager.getInstance().getWriteableCalendars();
|
||
|
|
if (calendars.isEmpty()) {
|
||
|
|
calendars = new ArrayList<>();
|
||
|
|
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);
|
||
|
|
view.setBackgroundColor(Color.parseColor(getCalendarColor(getItem(position))));
|
||
|
|
view.setTextColor(Color.WHITE);
|
||
|
|
view.setTypeface(null, Typeface.BOLD);
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
@Override public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||
|
|
TextView view = (TextView) super.getDropDownView(position, convertView, parent);
|
||
|
|
view.setTextColor(Color.parseColor(getCalendarColor(getItem(position))));
|
||
|
|
view.setTypeface(null, Typeface.BOLD);
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||
|
|
spinnerCalendar.setAdapter(adapter);
|
||
|
|
}
|
||
|
|
|
||
|
|
private String getCalendarColor(String name) {
|
||
|
|
if (name == null) return "#888888";
|
||
|
|
switch (name) {
|
||
|
|
case "Felles": return "#0069B3"; // KBS Blå
|
||
|
|
case "AMU/HMS/Miljø": return "#2E7D32"; // Mørk grønn (Miljø-profil)
|
||
|
|
case "Administrasjonen": return "#607D8B";
|
||
|
|
case "Serviceavdelingen": return "#E65100";
|
||
|
|
case "Automasjonsavdelingen": return "#2E7D32"; // Merk: Denne er lik Miljø nå, du kan bytte til f.eks #1B5E20 hvis ønskelig
|
||
|
|
case "Prosjektavdelingen": return "#7B1FA2";
|
||
|
|
default: return "#888888";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void setupReminderChips() {
|
||
|
|
chipGroupReminders.removeAllViews();
|
||
|
|
addReminderChip("Ved start", 0);
|
||
|
|
addReminderChip("5 min", 5);
|
||
|
|
addReminderChip("10 min", 10);
|
||
|
|
addReminderChip("15 min", 15);
|
||
|
|
addReminderChip("30 min", 30);
|
||
|
|
addReminderChip("1 t", 60);
|
||
|
|
addReminderChip("2 t", 120);
|
||
|
|
addReminderChip("1 d", 1440);
|
||
|
|
addReminderChip("2 d", 2880);
|
||
|
|
addReminderChip("1 u", 10080);
|
||
|
|
|
||
|
|
if (eventToEdit == null) {
|
||
|
|
for (int i=0; i<chipGroupReminders.getChildCount(); i++) {
|
||
|
|
Chip c = (Chip) chipGroupReminders.getChildAt(i);
|
||
|
|
if ((int)c.getTag() == 15) c.setChecked(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void addReminderChip(String text, int minutes) {
|
||
|
|
Chip chip = new Chip(getContext());
|
||
|
|
chip.setText(text);
|
||
|
|
chip.setTag(minutes);
|
||
|
|
chip.setCheckable(true);
|
||
|
|
chipGroupReminders.addView(chip);
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
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("Månedlig den " + weekNo + ". " + dayName + "en");
|
||
|
|
options.add("Årlig den " + dayOfMonth + ". " + monthName); options.add("Hver ukedag (man-fre)"); options.add("Egendefinert...");
|
||
|
|
spinnerRecurrence.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, options));
|
||
|
|
}
|
||
|
|
|
||
|
|
private void generateStandardRRule(int position) {
|
||
|
|
switch (position) {
|
||
|
|
case 1: selectedRRule = "RRULE:FREQ=DAILY"; break;
|
||
|
|
case 2: selectedRRule = "RRULE:FREQ=WEEKLY"; break;
|
||
|
|
case 3: selectedRRule = "RRULE:FREQ=MONTHLY"; break;
|
||
|
|
case 4: selectedRRule = "RRULE:FREQ=MONTHLY;BYDAY=" + ((startCal.get(Calendar.DAY_OF_MONTH)-1)/7+1) + getDayCode(startCal.get(Calendar.DAY_OF_WEEK)); break;
|
||
|
|
case 5: selectedRRule = "RRULE:FREQ=YEARLY"; break;
|
||
|
|
case 6: selectedRRule = "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"; break;
|
||
|
|
default: selectedRRule = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private String getDayCode(int calendarDay) {
|
||
|
|
switch (calendarDay) {
|
||
|
|
case Calendar.MONDAY: return "MO"; case Calendar.TUESDAY: return "TU"; case Calendar.WEDNESDAY: return "WE";
|
||
|
|
case Calendar.THURSDAY: return "TH"; case Calendar.FRIDAY: return "FR"; case Calendar.SATURDAY: return "SA";
|
||
|
|
case Calendar.SUNDAY: return "SU"; default: return "MO";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showCustomRecurrenceDialog() {
|
||
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||
|
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_custom_recurrence, null);
|
||
|
|
EditText etInterval = view.findViewById(R.id.et_interval), etCount = view.findViewById(R.id.et_count);
|
||
|
|
Spinner spinnerFreq = view.findViewById(R.id.spinner_freq);
|
||
|
|
View layoutWeekdays = view.findViewById(R.id.layout_weekdays);
|
||
|
|
ToggleButton[] toggles = {view.findViewById(R.id.tg_mon), view.findViewById(R.id.tg_tue), view.findViewById(R.id.tg_wed), view.findViewById(R.id.tg_thu), view.findViewById(R.id.tg_fri), view.findViewById(R.id.tg_sat), view.findViewById(R.id.tg_sun)};
|
||
|
|
String[] labels = {"M", "T", "O", "T", "F", "L", "S"};
|
||
|
|
for(int i=0; i<7; i++) { toggles[i].setText(labels[i]); toggles[i].setTextOn(labels[i]); toggles[i].setTextOff(labels[i]); }
|
||
|
|
RadioGroup rgEnd = view.findViewById(R.id.rg_end); Button btnEndDatePicker = view.findViewById(R.id.btn_end_date_picker);
|
||
|
|
Calendar customEndCal = Calendar.getInstance(); customEndCal.add(Calendar.MONTH, 1);
|
||
|
|
btnEndDatePicker.setText(new SimpleDateFormat("d. MMM yyyy", Locale.getDefault()).format(customEndCal.getTime()));
|
||
|
|
btnEndDatePicker.setOnClickListener(v -> {
|
||
|
|
rgEnd.check(R.id.rb_date);
|
||
|
|
new DatePickerDialog(getContext(), (p, y, m, d) -> { customEndCal.set(y, m, d); btnEndDatePicker.setText(new SimpleDateFormat("d. MMM yyyy", Locale.getDefault()).format(customEndCal.getTime())); }, customEndCal.get(Calendar.YEAR), customEndCal.get(Calendar.MONTH), customEndCal.get(Calendar.DAY_OF_MONTH)).show();
|
||
|
|
});
|
||
|
|
spinnerFreq.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||
|
|
@Override public void onItemSelected(AdapterView<?> p, View v, int pos, long id) { layoutWeekdays.setVisibility(pos == 1 ? View.VISIBLE : View.GONE); }
|
||
|
|
@Override public void onNothingSelected(AdapterView<?> p) {}
|
||
|
|
});
|
||
|
|
builder.setView(view); AlertDialog dialog = builder.create();
|
||
|
|
view.findViewById(R.id.btn_cancel).setOnClickListener(v -> { dialog.dismiss(); spinnerRecurrence.setSelection(0); });
|
||
|
|
view.findViewById(R.id.btn_done).setOnClickListener(v -> {
|
||
|
|
StringBuilder rrule = new StringBuilder("RRULE:FREQ=");
|
||
|
|
rrule.append(new String[]{"DAILY", "WEEKLY", "MONTHLY", "YEARLY"}[spinnerFreq.getSelectedItemPosition()]);
|
||
|
|
if (!etInterval.getText().toString().isEmpty() && !etInterval.getText().toString().equals("1")) rrule.append(";INTERVAL=").append(etInterval.getText().toString());
|
||
|
|
if (spinnerFreq.getSelectedItemPosition() == 1) {
|
||
|
|
List<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));
|
||
|
|
}
|
||
|
|
if (rgEnd.getCheckedRadioButtonId() == R.id.rb_date) {
|
||
|
|
customEndCal.set(Calendar.HOUR_OF_DAY, 23); customEndCal.set(Calendar.MINUTE, 59);
|
||
|
|
SimpleDateFormat utc = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); utc.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
|
||
|
|
rrule.append(";UNTIL=").append(utc.format(customEndCal.getTime()));
|
||
|
|
} else if (rgEnd.getCheckedRadioButtonId() == R.id.rb_count) rrule.append(";COUNT=").append(etCount.getText().toString());
|
||
|
|
selectedRRule = rrule.toString(); isCustomRecurrence = true; dialog.dismiss();
|
||
|
|
});
|
||
|
|
dialog.show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void pickDate(Calendar cal, boolean isStart) {
|
||
|
|
new DatePickerDialog(getContext(), (view, y, m, d) -> {
|
||
|
|
cal.set(y, m, d);
|
||
|
|
if (isStart) {
|
||
|
|
if (endCal.before(startCal)) { endCal.setTime(startCal.getTime()); if (!switchAllDay.isChecked()) endCal.add(Calendar.HOUR_OF_DAY, 1); }
|
||
|
|
updateRecurrenceSpinner(); if (!isCustomRecurrence) spinnerRecurrence.setSelection(0);
|
||
|
|
}
|
||
|
|
updateUI();
|
||
|
|
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void pickTime(Calendar cal) {
|
||
|
|
new TimePickerDialog(getContext(), (view, h, m) -> {
|
||
|
|
cal.set(Calendar.HOUR_OF_DAY, h); cal.set(Calendar.MINUTE, m);
|
||
|
|
if (cal == startCal && endCal.before(startCal)) { endCal.setTime(startCal.getTime()); endCal.add(Calendar.HOUR_OF_DAY, 1); }
|
||
|
|
updateUI();
|
||
|
|
}, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), true).show();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateUI() {
|
||
|
|
boolean isAllDay = switchAllDay.isChecked();
|
||
|
|
btnStartTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE);
|
||
|
|
btnEndTime.setVisibility(isAllDay ? View.GONE : View.VISIBLE);
|
||
|
|
SimpleDateFormat dFmt = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()), tFmt = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||
|
|
btnStartDate.setText(dFmt.format(startCal.getTime())); btnEndDate.setText(dFmt.format(endCal.getTime()));
|
||
|
|
btnStartTime.setText(tFmt.format(startCal.getTime())); btnEndTime.setText(tFmt.format(endCal.getTime()));
|
||
|
|
String p = dFmt.format(startCal.getTime()) + (isAllDay ? "" : " " + tFmt.format(startCal.getTime())) + " - ";
|
||
|
|
if (startCal.get(Calendar.YEAR) == endCal.get(Calendar.YEAR) && startCal.get(Calendar.DAY_OF_YEAR) == endCal.get(Calendar.DAY_OF_YEAR)) {
|
||
|
|
p += (isAllDay ? "(Samme dag)" : tFmt.format(endCal.getTime()));
|
||
|
|
} else p += dFmt.format(endCal.getTime()) + (isAllDay ? "" : " " + tFmt.format(endCal.getTime()));
|
||
|
|
txtPreview.setText(p);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void submitEvent() {
|
||
|
|
String title = etTitle.getText().toString().trim();
|
||
|
|
if (title.isEmpty()) { etTitle.setError("Mangler tittel"); return; }
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat(switchAllDay.isChecked() ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm", Locale.getDefault());
|
||
|
|
List<Integer> reminders = new ArrayList<>();
|
||
|
|
for (int i=0; i<chipGroupReminders.getChildCount(); i++) {
|
||
|
|
Chip c = (Chip) chipGroupReminders.getChildAt(i);
|
||
|
|
if (c.isChecked()) reminders.add((Integer) c.getTag());
|
||
|
|
}
|
||
|
|
String desc = etDesc.getText().toString();
|
||
|
|
if (rbSpecific.isChecked()) {
|
||
|
|
StringBuilder sb = new StringBuilder(); boolean first = true;
|
||
|
|
for (User u : filteredUsers) if (u.isSelected()) { if (!first) sb.append(","); sb.append(u.getEmail()); first = false; }
|
||
|
|
if (sb.length() > 0) desc += "\n\n#deltakere:" + sb.toString();
|
||
|
|
}
|
||
|
|
desc += "\n#arrangor:" + (originalOrganizer != null ? originalOrganizer : UserManager.getInstance().getUserDisplayName());
|
||
|
|
|
||
|
|
String calSlug = spinnerCalendar.getSelectedItem() != null ? spinnerCalendar.getSelectedItem().toString() : "Felles";
|
||
|
|
|
||
|
|
CreateEventRequest req = new CreateEventRequest(title, desc, etLocation.getText().toString(), sdf.format(startCal.getTime()), sdf.format(endCal.getTime()), calSlug, reminders, switchAllDay.isChecked(), selectedRRule);
|
||
|
|
if (eventToEdit != null) req.id = eventToEdit.getId();
|
||
|
|
|
||
|
|
Call<JsonElement> call = (eventToEdit != null) ? RetrofitClient.getApiService().updateCalendarEvent(req) : 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(), "Lagret!", Toast.LENGTH_LONG).show();
|
||
|
|
Navigation.findNavController(getView()).navigateUp();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override public void onFailure(Call<JsonElement> call, Throwable t) { Toast.makeText(getContext(), "Feil!", Toast.LENGTH_SHORT).show(); }
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\CreateEventRequest.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import com.google.gson.annotations.SerializedName;
|
||
|
|
import java.util.List;
|
||
|
|
public class CreateEventRequest {
|
||
|
|
@SerializedName("id")
|
||
|
|
public String id;
|
||
|
|
@SerializedName("title")
|
||
|
|
public String title;
|
||
|
|
|
||
|
|
@SerializedName("description")
|
||
|
|
public String description;
|
||
|
|
|
||
|
|
@SerializedName("location")
|
||
|
|
public String location;
|
||
|
|
|
||
|
|
@SerializedName("start_time")
|
||
|
|
public String startTime;
|
||
|
|
|
||
|
|
@SerializedName("end_time")
|
||
|
|
public String endTime;
|
||
|
|
|
||
|
|
@SerializedName("calendar_type")
|
||
|
|
public String calendarType;
|
||
|
|
|
||
|
|
@SerializedName("reminders")
|
||
|
|
public List<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\HomeAdapter.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.content.res.ColorStateList;
|
||
|
|
import android.graphics.Color;
|
||
|
|
import android.graphics.Paint;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.Button;
|
||
|
|
import android.widget.CheckBox;
|
||
|
|
import android.widget.ImageView;
|
||
|
|
import android.widget.LinearLayout;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.cardview.widget.CardView;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import com.bumptech.glide.Glide;
|
||
|
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
|
||
|
|
public class HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||
|
|
|
||
|
|
public static final int TYPE_CREATE_BUTTON = 1;
|
||
|
|
public static final int TYPE_SECTION_TITLE = 2;
|
||
|
|
public static final int TYPE_CALENDAR_ITEM = 3;
|
||
|
|
public static final int TYPE_NEWS_ITEM = 4;
|
||
|
|
public static final int TYPE_TASK_ITEM = 5;
|
||
|
|
public static final int TYPE_EMPTY_TASKS = 6;
|
||
|
|
|
||
|
|
private final List<Object> items;
|
||
|
|
private final OnHomeClickListener listener;
|
||
|
|
|
||
|
|
public interface OnHomeClickListener {
|
||
|
|
void onCreateEventClick();
|
||
|
|
void onViewAllCalendarClick();
|
||
|
|
void onViewAllNewsClick();
|
||
|
|
void onViewAllTasksClick();
|
||
|
|
void onCalendarItemClick(CalendarEvent event);
|
||
|
|
void onNewsItemClick(WpPost post);
|
||
|
|
void onTaskItemClick(TaskItem task);
|
||
|
|
void onTaskStatusChanged(TaskItem task, boolean isDone);
|
||
|
|
}
|
||
|
|
|
||
|
|
public HomeAdapter(List<Object> items, OnHomeClickListener listener) {
|
||
|
|
this.items = items;
|
||
|
|
this.listener = listener;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int getItemViewType(int position) {
|
||
|
|
Object item = items.get(position);
|
||
|
|
if (item instanceof CreateButtonItem) return TYPE_CREATE_BUTTON;
|
||
|
|
if (item instanceof SectionTitleItem) return TYPE_SECTION_TITLE;
|
||
|
|
if (item instanceof CalendarEvent) return TYPE_CALENDAR_ITEM;
|
||
|
|
if (item instanceof WpPost) return TYPE_NEWS_ITEM;
|
||
|
|
if (item instanceof TaskItem) return TYPE_TASK_ITEM;
|
||
|
|
if (item instanceof EmptyTasksItem) return TYPE_EMPTY_TASKS;
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
@NonNull
|
||
|
|
@Override
|
||
|
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||
|
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||
|
|
switch (viewType) {
|
||
|
|
case TYPE_CREATE_BUTTON:
|
||
|
|
return new CreateButtonViewHolder(inflater.inflate(R.layout.item_home_create_btn, parent, false));
|
||
|
|
case TYPE_SECTION_TITLE:
|
||
|
|
return new SectionTitleViewHolder(inflater.inflate(R.layout.item_home_section_title, parent, false));
|
||
|
|
case TYPE_CALENDAR_ITEM:
|
||
|
|
return new CalendarViewHolder(inflater.inflate(R.layout.item_calendar, parent, false));
|
||
|
|
case TYPE_TASK_ITEM:
|
||
|
|
return new TaskViewHolder(inflater.inflate(R.layout.item_task, parent, false));
|
||
|
|
case TYPE_EMPTY_TASKS:
|
||
|
|
return new EmptyViewHolder(inflater.inflate(R.layout.item_home_empty_tasks, parent, false));
|
||
|
|
case TYPE_NEWS_ITEM:
|
||
|
|
return new NewsViewHolder(inflater.inflate(R.layout.item_news, parent, false));
|
||
|
|
default:
|
||
|
|
throw new IllegalArgumentException("Ugyldig viewType");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||
|
|
Object item = items.get(position);
|
||
|
|
|
||
|
|
if (holder instanceof CreateButtonViewHolder) {
|
||
|
|
((CreateButtonViewHolder) holder).btnCreate.setOnClickListener(v -> listener.onCreateEventClick());
|
||
|
|
}
|
||
|
|
else if (holder instanceof SectionTitleViewHolder) {
|
||
|
|
SectionTitleItem section = (SectionTitleItem) item;
|
||
|
|
SectionTitleViewHolder vh = (SectionTitleViewHolder) holder;
|
||
|
|
vh.title.setText(section.title);
|
||
|
|
vh.btnViewAll.setOnClickListener(v -> {
|
||
|
|
if (section.type == SectionTitleItem.TYPE_CALENDAR) listener.onViewAllCalendarClick();
|
||
|
|
else if (section.type == SectionTitleItem.TYPE_TASKS) listener.onViewAllTasksClick();
|
||
|
|
else listener.onViewAllNewsClick();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
else if (holder instanceof CalendarViewHolder) {
|
||
|
|
CalendarEvent event = (CalendarEvent) item;
|
||
|
|
CalendarViewHolder vh = (CalendarViewHolder) holder;
|
||
|
|
vh.day.setText(event.getDay());
|
||
|
|
vh.month.setText(event.getMonth());
|
||
|
|
vh.time.setText(event.getTime());
|
||
|
|
vh.title.setText(event.getTitle());
|
||
|
|
boolean isPrivate = event.getDescription() != null && event.getDescription().contains("#deltakere:");
|
||
|
|
try {
|
||
|
|
int color = Color.parseColor(isPrivate ? "#673AB7" : event.getCalendarColor());
|
||
|
|
vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(color));
|
||
|
|
} catch (Exception e) {
|
||
|
|
vh.dateBox.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#0069B3")));
|
||
|
|
}
|
||
|
|
vh.itemView.setOnClickListener(v -> listener.onCalendarItemClick(event));
|
||
|
|
}
|
||
|
|
else if (holder instanceof TaskViewHolder) {
|
||
|
|
TaskItem task = (TaskItem) item;
|
||
|
|
TaskViewHolder vh = (TaskViewHolder) holder;
|
||
|
|
vh.title.setText(task.getTitle());
|
||
|
|
|
||
|
|
if (task.getDueDate() > 0) {
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault());
|
||
|
|
vh.date.setText("Frist: " + sdf.format(new Date(task.getDueDate())));
|
||
|
|
} else {
|
||
|
|
vh.date.setText("Ingen frist");
|
||
|
|
}
|
||
|
|
|
||
|
|
String myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
boolean myStatus = task.getParticipantStatus(myEmail);
|
||
|
|
vh.checkBox.setChecked(myStatus);
|
||
|
|
|
||
|
|
long now = System.currentTimeMillis();
|
||
|
|
boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !myStatus;
|
||
|
|
|
||
|
|
if (isOverdue) {
|
||
|
|
vh.cardView.setCardBackgroundColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_soft_light_pink_beige));
|
||
|
|
vh.date.setTextColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_logo_accent_red));
|
||
|
|
} else {
|
||
|
|
vh.cardView.setCardBackgroundColor(Color.WHITE);
|
||
|
|
vh.date.setTextColor(ContextCompat.getColor(vh.itemView.getContext(), R.color.kbs_muted_blue_gray));
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- REINTRODUSERT FREMDRIFTSBEREGNING ---
|
||
|
|
int total = task.getAssigneeStatus().size();
|
||
|
|
int done = 0;
|
||
|
|
for (Boolean isFinished : task.getAssigneeStatus().values()) {
|
||
|
|
if (isFinished) done++;
|
||
|
|
}
|
||
|
|
vh.progress.setText("Fremdrift: " + done + "/" + total);
|
||
|
|
// ------------------------------------------
|
||
|
|
|
||
|
|
vh.checkBox.setOnClickListener(v -> listener.onTaskStatusChanged(task, vh.checkBox.isChecked()));
|
||
|
|
vh.itemView.setOnClickListener(v -> listener.onTaskItemClick(task));
|
||
|
|
}
|
||
|
|
else if (holder instanceof NewsViewHolder) {
|
||
|
|
WpPost post = (WpPost) item;
|
||
|
|
NewsViewHolder vh = (NewsViewHolder) holder;
|
||
|
|
vh.title.setText(post.getTitleStr());
|
||
|
|
vh.excerpt.setText(post.getExcerptStr());
|
||
|
|
vh.date.setText(post.date);
|
||
|
|
String cat = post.getCategoryName();
|
||
|
|
vh.category.setText(cat);
|
||
|
|
vh.category.setVisibility(cat.isEmpty() ? View.GONE : View.VISIBLE);
|
||
|
|
String imgUrl = post.getFeaturedImageUrl();
|
||
|
|
if (imgUrl != null) {
|
||
|
|
vh.image.setVisibility(View.VISIBLE);
|
||
|
|
Glide.with(vh.image.getContext()).load(imgUrl).transition(DrawableTransitionOptions.withCrossFade()).centerCrop().into(vh.image);
|
||
|
|
} else {
|
||
|
|
vh.image.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
vh.itemView.setOnClickListener(v -> listener.onNewsItemClick(post));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int getItemCount() {
|
||
|
|
return items.size();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- VIEW HOLDERS ---
|
||
|
|
static class CreateButtonViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
Button btnCreate;
|
||
|
|
CreateButtonViewHolder(View v) { super(v); btnCreate = v.findViewById(R.id.btn_create_event); }
|
||
|
|
}
|
||
|
|
|
||
|
|
static class SectionTitleViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView title, btnViewAll;
|
||
|
|
SectionTitleViewHolder(View v) { super(v); title = v.findViewById(R.id.txt_section_title); btnViewAll = v.findViewById(R.id.btn_view_all); }
|
||
|
|
}
|
||
|
|
|
||
|
|
static class CalendarViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView day, month, title, time;
|
||
|
|
LinearLayout dateBox;
|
||
|
|
CalendarViewHolder(View view) {
|
||
|
|
super(view);
|
||
|
|
day = view.findViewById(R.id.cal_day);
|
||
|
|
month = view.findViewById(R.id.cal_month);
|
||
|
|
title = view.findViewById(R.id.cal_title);
|
||
|
|
time = view.findViewById(R.id.cal_time);
|
||
|
|
dateBox = view.findViewById(R.id.date_box_background);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static class TaskViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView title, date, progress;
|
||
|
|
CheckBox checkBox;
|
||
|
|
CardView cardView;
|
||
|
|
TaskViewHolder(View v) {
|
||
|
|
super(v);
|
||
|
|
title = v.findViewById(R.id.task_title);
|
||
|
|
date = v.findViewById(R.id.task_date);
|
||
|
|
progress = v.findViewById(R.id.task_progress); // Husk denne
|
||
|
|
checkBox = v.findViewById(R.id.task_checkbox);
|
||
|
|
cardView = (CardView) v;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static class NewsViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView title, excerpt, date, category;
|
||
|
|
ImageView image;
|
||
|
|
NewsViewHolder(View v) {
|
||
|
|
super(v);
|
||
|
|
title = v.findViewById(R.id.news_title);
|
||
|
|
excerpt = v.findViewById(R.id.news_excerpt);
|
||
|
|
date = v.findViewById(R.id.news_date);
|
||
|
|
category = v.findViewById(R.id.news_category);
|
||
|
|
image = v.findViewById(R.id.news_image);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static class EmptyViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
EmptyViewHolder(View v) { super(v); }
|
||
|
|
}
|
||
|
|
|
||
|
|
public static class CreateButtonItem {}
|
||
|
|
public static class EmptyTasksItem {}
|
||
|
|
public static class SectionTitleItem {
|
||
|
|
public static final int TYPE_CALENDAR = 0;
|
||
|
|
public static final int TYPE_NEWS = 1;
|
||
|
|
public static final int TYPE_TASKS = 2;
|
||
|
|
String title; int type;
|
||
|
|
public SectionTitleItem(String t, int type) { this.title = t; this.type = type; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.Manifest;
|
||
|
|
import android.content.pm.PackageManager;
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.os.Handler;
|
||
|
|
import android.os.Looper;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.ProgressBar;
|
||
|
|
import androidx.activity.result.ActivityResultLauncher;
|
||
|
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import androidx.fragment.app.Fragment;
|
||
|
|
import androidx.navigation.Navigation;
|
||
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.Collections;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
import com.google.gson.JsonElement;
|
||
|
|
|
||
|
|
public class HomeFragment extends Fragment implements HomeAdapter.OnHomeClickListener {
|
||
|
|
|
||
|
|
private RecyclerView recyclerView;
|
||
|
|
private HomeAdapter adapter;
|
||
|
|
private ProgressBar mainProgressBar;
|
||
|
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||
|
|
|
||
|
|
private List<CalendarEvent> currentEvents = new ArrayList<>();
|
||
|
|
private List<WpPost> currentNews = new ArrayList<>();
|
||
|
|
private List<TaskItem> currentTasks = new ArrayList<>();
|
||
|
|
|
||
|
|
private int activeNetworkCalls = 0;
|
||
|
|
// Cache lever i 60 minutter. PUSH vil ugyldiggjøre den før tiden ved behov.
|
||
|
|
private static final int CACHE_TTL_MINUTES = 60;
|
||
|
|
|
||
|
|
private Handler timeoutHandler = new Handler(Looper.getMainLooper());
|
||
|
|
private Runnable timeoutRunnable = this::forceStopLoading;
|
||
|
|
|
||
|
|
private ActivityResultLauncher<String> requestPermissionLauncher;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||
|
|
super.onCreate(savedInstanceState);
|
||
|
|
requestPermissionLauncher = registerForActivityResult(
|
||
|
|
new ActivityResultContracts.RequestPermission(),
|
||
|
|
isGranted -> refreshData(true)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
return inflater.inflate(R.layout.fragment_home, container, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||
|
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
|
||
|
|
mainProgressBar = view.findViewById(R.id.main_loading_spinner);
|
||
|
|
swipeRefreshLayout = view.findViewById(R.id.swipe_refresh_home);
|
||
|
|
recyclerView = view.findViewById(R.id.main_recycler_view);
|
||
|
|
|
||
|
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||
|
|
|
||
|
|
// Ved manuell swipe: Tving nettverksoppdatering
|
||
|
|
swipeRefreshLayout.setOnRefreshListener(() -> refreshData(true));
|
||
|
|
|
||
|
|
// Ved oppstart: Bruk cache (false = ikke tving nettverk)
|
||
|
|
refreshData(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onDestroyView() {
|
||
|
|
super.onDestroyView();
|
||
|
|
timeoutHandler.removeCallbacks(timeoutRunnable);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void refreshData(boolean forceNetwork) {
|
||
|
|
// 1. Last cache i bakgrunnen
|
||
|
|
new Thread(() -> {
|
||
|
|
if (getContext() == null) return;
|
||
|
|
|
||
|
|
// Hent data fra disk
|
||
|
|
List<CalendarEvent> cachedApiEvents = CacheManager.getCachedCalendarEvents(getContext());
|
||
|
|
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true);
|
||
|
|
|
||
|
|
for (CalendarEvent e : cachedApiEvents) CalendarManager.formatEventForUI(e);
|
||
|
|
List<CalendarEvent> mergedEvents = CalendarManager.mergeAndSort(cachedApiEvents, deviceEvents);
|
||
|
|
|
||
|
|
List<WpPost> cachedNews = CacheManager.getCachedNewsPosts(getContext());
|
||
|
|
formatNewsDates(cachedNews);
|
||
|
|
|
||
|
|
List<TaskItem> cachedTasks = CacheManager.getTasks(getContext());
|
||
|
|
|
||
|
|
// 2. Oppdater UI på hovedtråden
|
||
|
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||
|
|
if (!isAdded()) return;
|
||
|
|
|
||
|
|
// Oppdater variablene
|
||
|
|
currentEvents = mergedEvents;
|
||
|
|
currentNews = cachedNews;
|
||
|
|
currentTasks = cachedTasks;
|
||
|
|
|
||
|
|
// Vis dataene vi fant umiddelbart (fjerner spinner hvis den var der)
|
||
|
|
buildAndDisplayList();
|
||
|
|
|
||
|
|
// 3. Vurder om vi skal hente nytt fra nettet
|
||
|
|
boolean cacheValidCal = CacheManager.isCacheValid(getContext(), "calendar", CACHE_TTL_MINUTES);
|
||
|
|
boolean cacheValidNews = CacheManager.isCacheValid(getContext(), "news", CACHE_TTL_MINUTES);
|
||
|
|
boolean cacheValidTasks = CacheManager.isCacheValid(getContext(), "tasks", CACHE_TTL_MINUTES);
|
||
|
|
|
||
|
|
// Vi henter KUN hvis brukeren swiper (force) ELLER cachen er utgått på dato.
|
||
|
|
// Vi henter IKKE bare fordi listene er tomme (da stoler vi på at cachen er korrekt tom).
|
||
|
|
boolean needNetwork = forceNetwork || !cacheValidCal || !cacheValidNews || !cacheValidTasks;
|
||
|
|
|
||
|
|
if (needNetwork) {
|
||
|
|
// Vis spinner KUN hvis det er helt tomt fra før, eller ved manuell swipe
|
||
|
|
boolean isEverythingEmpty = currentNews.isEmpty() && currentTasks.isEmpty() && currentEvents.isEmpty();
|
||
|
|
|
||
|
|
if (isEverythingEmpty && mainProgressBar != null) {
|
||
|
|
mainProgressBar.setVisibility(View.VISIBLE);
|
||
|
|
} else if (forceNetwork && swipeRefreshLayout != null) {
|
||
|
|
swipeRefreshLayout.setRefreshing(true);
|
||
|
|
}
|
||
|
|
// Ellers: Silent update (ingen spinner, men vi henter i bakgrunnen)
|
||
|
|
|
||
|
|
activeNetworkCalls = 3;
|
||
|
|
timeoutHandler.removeCallbacks(timeoutRunnable);
|
||
|
|
timeoutHandler.postDelayed(timeoutRunnable, 10000); // 10 sek timeout
|
||
|
|
|
||
|
|
fetchCalendarData();
|
||
|
|
fetchNewsData();
|
||
|
|
fetchTaskData();
|
||
|
|
} else {
|
||
|
|
// Cachen er god nok! Skjul evt spinnere.
|
||
|
|
stopLoaders();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}).start();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void stopLoaders() {
|
||
|
|
if (mainProgressBar != null) mainProgressBar.setVisibility(View.GONE);
|
||
|
|
if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void forceStopLoading() {
|
||
|
|
if (activeNetworkCalls > 0) {
|
||
|
|
activeNetworkCalls = 0;
|
||
|
|
stopLoaders();
|
||
|
|
buildAndDisplayList();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchCalendarData() {
|
||
|
|
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
|
||
|
|
requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR);
|
||
|
|
checkLoadingComplete();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
List<CalendarEvent> apiEvents = response.body();
|
||
|
|
CacheManager.saveCalendarEvents(getContext(), apiEvents);
|
||
|
|
|
||
|
|
new Thread(() -> {
|
||
|
|
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext(), true);
|
||
|
|
for (CalendarEvent e : apiEvents) CalendarManager.formatEventForUI(e);
|
||
|
|
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
|
||
|
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||
|
|
currentEvents = merged;
|
||
|
|
checkLoadingComplete();
|
||
|
|
});
|
||
|
|
}).start();
|
||
|
|
} else {
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchNewsData() {
|
||
|
|
RetrofitClient.getApiService().getPosts().enqueue(new Callback<List<WpPost>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
currentNews = response.body();
|
||
|
|
CacheManager.saveNewsPosts(getContext(), currentNews);
|
||
|
|
formatNewsDates(currentNews);
|
||
|
|
}
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<WpPost>> call, Throwable t) {
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchTaskData() {
|
||
|
|
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
currentTasks = response.body();
|
||
|
|
CacheManager.saveTasks(getContext(), currentTasks);
|
||
|
|
}
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<TaskItem>> call, Throwable t) {
|
||
|
|
checkLoadingComplete();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void formatNewsDates(List<WpPost> posts) {
|
||
|
|
if (posts == null) return;
|
||
|
|
SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||
|
|
SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
|
||
|
|
for (WpPost post : posts) {
|
||
|
|
try {
|
||
|
|
if (post.date != null && post.date.contains("T")) {
|
||
|
|
Date date = rawFormat.parse(post.date);
|
||
|
|
post.date = targetFormat.format(date);
|
||
|
|
}
|
||
|
|
} catch (Exception ignored) {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private synchronized void checkLoadingComplete() {
|
||
|
|
activeNetworkCalls--;
|
||
|
|
// Oppdater visningen fortløpende så snart data kommer inn
|
||
|
|
buildAndDisplayList();
|
||
|
|
|
||
|
|
if (activeNetworkCalls <= 0) {
|
||
|
|
activeNetworkCalls = 0;
|
||
|
|
timeoutHandler.removeCallbacks(timeoutRunnable);
|
||
|
|
stopLoaders();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void buildAndDisplayList() {
|
||
|
|
if (!isAdded()) return;
|
||
|
|
|
||
|
|
List<Object> items = new ArrayList<>();
|
||
|
|
String myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
|
||
|
|
// 1. KALENDER
|
||
|
|
items.add(new HomeAdapter.SectionTitleItem("Kommende hendelser", HomeAdapter.SectionTitleItem.TYPE_CALENDAR));
|
||
|
|
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
|
||
|
|
int calCount = 0;
|
||
|
|
if (currentEvents != null) {
|
||
|
|
for (CalendarEvent e : currentEvents) {
|
||
|
|
if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) {
|
||
|
|
items.add(e);
|
||
|
|
calCount++;
|
||
|
|
}
|
||
|
|
if (calCount >= 3) break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. OPPGAVER
|
||
|
|
items.add(new HomeAdapter.SectionTitleItem("Mine oppgaver", HomeAdapter.SectionTitleItem.TYPE_TASKS));
|
||
|
|
List<TaskItem> myActiveTasks = new ArrayList<>();
|
||
|
|
if (currentTasks != null) {
|
||
|
|
for (TaskItem t : currentTasks) {
|
||
|
|
// Vis oppgaver jeg er deltaker i, som ikke er fullført av meg, og ikke helt lukket
|
||
|
|
if (t.isUserParticipant(myEmail) && !t.getParticipantStatus(myEmail) && !t.isFullyCompleted()) {
|
||
|
|
myActiveTasks.add(t);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (myActiveTasks.isEmpty()) {
|
||
|
|
items.add(new HomeAdapter.EmptyTasksItem());
|
||
|
|
} else {
|
||
|
|
// Sortering: Dato (stigende), deretter de uten dato (alfabetisk)
|
||
|
|
Collections.sort(myActiveTasks, (t1, t2) -> {
|
||
|
|
boolean t1HasDate = t1.getDueDate() > 0;
|
||
|
|
boolean t2HasDate = t2.getDueDate() > 0;
|
||
|
|
|
||
|
|
if (t1HasDate && !t2HasDate) return -1;
|
||
|
|
if (!t1HasDate && t2HasDate) return 1;
|
||
|
|
if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle());
|
||
|
|
|
||
|
|
return Long.compare(t1.getDueDate(), t2.getDueDate());
|
||
|
|
});
|
||
|
|
|
||
|
|
for (int i = 0; i < Math.min(myActiveTasks.size(), 3); i++) {
|
||
|
|
items.add(myActiveTasks.get(i));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. NYHETER
|
||
|
|
items.add(new HomeAdapter.SectionTitleItem("Siste nytt", HomeAdapter.SectionTitleItem.TYPE_NEWS));
|
||
|
|
if (currentNews != null) {
|
||
|
|
items.addAll(currentNews);
|
||
|
|
}
|
||
|
|
|
||
|
|
adapter = new HomeAdapter(items, this);
|
||
|
|
recyclerView.setAdapter(adapter);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- NAVIGASJON & KLIKK ---
|
||
|
|
|
||
|
|
@Override public void onCreateEventClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_create_event); }
|
||
|
|
@Override public void onViewAllCalendarClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_calendar_full); }
|
||
|
|
@Override public void onViewAllNewsClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_news_full); }
|
||
|
|
@Override public void onViewAllTasksClick() { Navigation.findNavController(getView()).navigate(R.id.navigation_tasks); }
|
||
|
|
|
||
|
|
@Override public void onCalendarItemClick(CalendarEvent event) {
|
||
|
|
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
|
||
|
|
sheet.setOnEventChangeListener(() -> refreshData(true));
|
||
|
|
sheet.show(getParentFragmentManager(), "CalendarDetails");
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override public void onNewsItemClick(WpPost post) {
|
||
|
|
Bundle bundle = new Bundle();
|
||
|
|
bundle.putSerializable("post_data", post);
|
||
|
|
Navigation.findNavController(getView()).navigate(R.id.navigation_news_detail, bundle);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override public void onTaskItemClick(TaskItem task) {
|
||
|
|
TaskDetailsBottomSheet sheet = new TaskDetailsBottomSheet(task, new TaskDetailsBottomSheet.OnTaskChangeListener() {
|
||
|
|
@Override public void onTaskChanged() { saveAndSyncTasks(); }
|
||
|
|
@Override public void onTaskDeleted(TaskItem taskToDelete) {
|
||
|
|
currentTasks.remove(taskToDelete);
|
||
|
|
saveAndSyncTasks();
|
||
|
|
}
|
||
|
|
@Override public void onEditRequested(TaskItem taskToEdit) {
|
||
|
|
AddTaskBottomSheet editDialog = new AddTaskBottomSheet();
|
||
|
|
editDialog.setTaskToEdit(taskToEdit);
|
||
|
|
editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() {
|
||
|
|
@Override public void onTaskAdded(TaskItem task) {}
|
||
|
|
@Override public void onTaskUpdated(TaskItem task) {
|
||
|
|
saveAndSyncTasks();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
editDialog.show(getChildFragmentManager(), "EditTask");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
sheet.show(getChildFragmentManager(), "TaskDetails");
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override public void onTaskStatusChanged(TaskItem task, boolean isDone) {
|
||
|
|
String myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
task.setParticipantStatus(myEmail, isDone);
|
||
|
|
if (task.getCreatedByEmail().equalsIgnoreCase(myEmail) && isDone) {
|
||
|
|
task.setFullyCompleted(true);
|
||
|
|
}
|
||
|
|
saveAndSyncTasks();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void saveAndSyncTasks() {
|
||
|
|
CacheManager.saveTasks(getContext(), currentTasks);
|
||
|
|
buildAndDisplayList();
|
||
|
|
RetrofitClient.getApiService().syncTasks(currentTasks).enqueue(new Callback<JsonElement>() {
|
||
|
|
@Override public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {}
|
||
|
|
@Override public void onFailure(Call<JsonElement> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\ImageDialogFragment.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.app.Dialog;
|
||
|
|
import android.graphics.Color;
|
||
|
|
import android.graphics.drawable.ColorDrawable;
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.view.Window;
|
||
|
|
import android.widget.ImageButton;
|
||
|
|
import android.widget.ImageView;
|
||
|
|
import android.widget.ProgressBar;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.fragment.app.DialogFragment;
|
||
|
|
import com.bumptech.glide.Glide;
|
||
|
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||
|
|
|
||
|
|
public class ImageDialogFragment extends DialogFragment {
|
||
|
|
|
||
|
|
private static final String ARG_URL = "image_url";
|
||
|
|
|
||
|
|
public static ImageDialogFragment newInstance(String imageUrl) {
|
||
|
|
ImageDialogFragment fragment = new ImageDialogFragment();
|
||
|
|
Bundle args = new Bundle();
|
||
|
|
args.putString(ARG_URL, imageUrl);
|
||
|
|
fragment.setArguments(args);
|
||
|
|
return fragment;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onStart() {
|
||
|
|
super.onStart();
|
||
|
|
// Gjør dialogen fullskjerm
|
||
|
|
Dialog dialog = getDialog();
|
||
|
|
if (dialog != null) {
|
||
|
|
int width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||
|
|
int height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||
|
|
dialog.getWindow().setLayout(width, height);
|
||
|
|
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
View view = inflater.inflate(R.layout.fragment_image_dialog, container, false);
|
||
|
|
|
||
|
|
ImageView imageView = view.findViewById(R.id.full_screen_image);
|
||
|
|
ImageButton closeBtn = view.findViewById(R.id.btn_close_image);
|
||
|
|
ProgressBar progressBar = view.findViewById(R.id.loading_image);
|
||
|
|
|
||
|
|
String url = getArguments() != null ? getArguments().getString(ARG_URL) : null;
|
||
|
|
|
||
|
|
if (url != null) {
|
||
|
|
Glide.with(this)
|
||
|
|
.load(url)
|
||
|
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||
|
|
.listener(new com.bumptech.glide.request.RequestListener<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 android.widget.TextView;
|
||
|
|
|
||
|
|
import androidx.activity.result.ActivityResultLauncher;
|
||
|
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||
|
|
import androidx.appcompat.app.AppCompatActivity;
|
||
|
|
import androidx.appcompat.widget.Toolbar;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import androidx.core.view.GravityCompat;
|
||
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||
|
|
import androidx.navigation.NavController;
|
||
|
|
import androidx.navigation.fragment.NavHostFragment;
|
||
|
|
import androidx.navigation.ui.AppBarConfiguration;
|
||
|
|
import androidx.navigation.ui.NavigationUI;
|
||
|
|
import androidx.work.ExistingPeriodicWorkPolicy;
|
||
|
|
import androidx.work.PeriodicWorkRequest;
|
||
|
|
import androidx.work.WorkManager;
|
||
|
|
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignIn;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
|
||
|
|
import com.google.android.material.navigation.NavigationView;
|
||
|
|
|
||
|
|
import java.util.concurrent.TimeUnit;
|
||
|
|
|
||
|
|
public class MainActivity extends AppCompatActivity {
|
||
|
|
|
||
|
|
public static final String GOOGLE_WEB_CLIENT_ID = BuildConfig.WEB_CLIENT_ID;
|
||
|
|
private NavController navController;
|
||
|
|
private DrawerLayout drawerLayout;
|
||
|
|
private AppBarConfiguration appBarConfiguration;
|
||
|
|
private ActivityResultLauncher<String> requestPermissionLauncher;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
protected void onCreate(Bundle savedInstanceState) {
|
||
|
|
super.onCreate(savedInstanceState);
|
||
|
|
setContentView(R.layout.activity_main);
|
||
|
|
|
||
|
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||
|
|
setSupportActionBar(toolbar);
|
||
|
|
|
||
|
|
drawerLayout = findViewById(R.id.drawer_layout);
|
||
|
|
NavigationView navigationView = findViewById(R.id.nav_view);
|
||
|
|
|
||
|
|
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
|
||
|
|
.findFragmentById(R.id.nav_host_fragment);
|
||
|
|
|
||
|
|
if (navHostFragment != null) {
|
||
|
|
navController = navHostFragment.getNavController();
|
||
|
|
|
||
|
|
appBarConfiguration = new AppBarConfiguration.Builder(
|
||
|
|
R.id.navigation_home, R.id.navigation_calendar_full, R.id.navigation_tasks,
|
||
|
|
R.id.navigation_forms, R.id.navigation_news_full, R.id.navigation_handbook)
|
||
|
|
.setOpenableLayout(drawerLayout)
|
||
|
|
.build();
|
||
|
|
|
||
|
|
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
|
||
|
|
|
||
|
|
// Vi bruker en tilpasset listener for å sikre at Hjem og andre punkter alltid fungerer
|
||
|
|
navigationView.setNavigationItemSelectedListener(item -> {
|
||
|
|
boolean handled = NavigationUI.onNavDestinationSelected(item, navController);
|
||
|
|
if (handled || item.getItemId() == R.id.navigation_home) {
|
||
|
|
if (item.getItemId() == R.id.navigation_home) {
|
||
|
|
navController.navigate(R.id.navigation_home);
|
||
|
|
}
|
||
|
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||
|
|
}
|
||
|
|
return handled;
|
||
|
|
});
|
||
|
|
|
||
|
|
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
|
||
|
|
if (destination.getId() == R.id.navigation_login) {
|
||
|
|
toolbar.setVisibility(View.GONE);
|
||
|
|
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||
|
|
} else {
|
||
|
|
toolbar.setVisibility(View.VISIBLE);
|
||
|
|
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
|
||
|
|
updateNavHeader(navigationView);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
setupTaskReminders();
|
||
|
|
createNotificationChannel();
|
||
|
|
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {});
|
||
|
|
checkNotificationPermission();
|
||
|
|
checkExactAlarmPermission();
|
||
|
|
checkLoginState();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateNavHeader(NavigationView navigationView) {
|
||
|
|
View headerView = navigationView.getHeaderView(0);
|
||
|
|
TextView name = headerView.findViewById(R.id.nav_header_name);
|
||
|
|
TextView email = headerView.findViewById(R.id.nav_header_email);
|
||
|
|
UserManager user = UserManager.getInstance();
|
||
|
|
if (user.isLoggedIn()) {
|
||
|
|
name.setText(user.getUserDisplayName());
|
||
|
|
email.setText(user.getUserEmail());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void setupTaskReminders() {
|
||
|
|
PeriodicWorkRequest taskCheck = new PeriodicWorkRequest.Builder(
|
||
|
|
TaskReminderWorker.class, 2, TimeUnit.DAYS).build();
|
||
|
|
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||
|
|
"TaskReminder", ExistingPeriodicWorkPolicy.KEEP, taskCheck);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public boolean onSupportNavigateUp() {
|
||
|
|
return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onBackPressed() {
|
||
|
|
if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||
|
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||
|
|
} else {
|
||
|
|
super.onBackPressed();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void checkLoginState() {
|
||
|
|
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
|
||
|
|
if (account == null) navigateToLogin();
|
||
|
|
else refreshGoogleToken();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void refreshGoogleToken() {
|
||
|
|
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||
|
|
.requestIdToken(GOOGLE_WEB_CLIENT_ID).requestEmail().build();
|
||
|
|
GoogleSignInClient client = GoogleSignIn.getClient(this, gso);
|
||
|
|
client.silentSignIn().addOnSuccessListener(account -> {
|
||
|
|
String photoUrl = (account.getPhotoUrl() != null) ? account.getPhotoUrl().toString() : null;
|
||
|
|
AuthRepository.loginToWordPress(account.getIdToken(), account.getDisplayName(), account.getEmail(), photoUrl,
|
||
|
|
new AuthRepository.AuthCallback() {
|
||
|
|
@Override public void onSuccess(String role) {
|
||
|
|
if (navController.getCurrentDestination().getId() == R.id.navigation_login) {
|
||
|
|
navController.navigate(R.id.action_login_to_home);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override public void onError(String message) { navigateToLogin(); }
|
||
|
|
});
|
||
|
|
}).addOnFailureListener(e -> navigateToLogin());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void navigateToLogin() {
|
||
|
|
if (navController != null && navController.getCurrentDestination() != null &&
|
||
|
|
navController.getCurrentDestination().getId() != R.id.navigation_login) {
|
||
|
|
navController.navigate(R.id.navigation_login);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void createNotificationChannel() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
|
NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", "Varsler", NotificationManager.IMPORTANCE_HIGH);
|
||
|
|
getSystemService(NotificationManager.class).createNotificationChannel(channel);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void checkNotificationPermission() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
|
|
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||
|
|
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void checkExactAlarmPermission() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||
|
|
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||
|
|
if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) {
|
||
|
|
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
|
||
|
|
intent.setData(Uri.parse("package:" + getPackageName()));
|
||
|
|
startActivity(intent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\MyFirebaseMessagingService.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.app.NotificationChannel;
|
||
|
|
import android.app.NotificationManager;
|
||
|
|
import android.app.PendingIntent;
|
||
|
|
import android.content.Context;
|
||
|
|
import android.content.Intent;
|
||
|
|
import android.content.pm.PackageManager;
|
||
|
|
import android.os.Build;
|
||
|
|
import android.util.Log;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.core.app.ActivityCompat;
|
||
|
|
import androidx.core.app.NotificationCompat;
|
||
|
|
import androidx.core.app.NotificationManagerCompat;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
||
|
|
import com.google.firebase.messaging.RemoteMessage;
|
||
|
|
import java.util.List;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class MyFirebaseMessagingService extends FirebaseMessagingService {
|
||
|
|
|
||
|
|
private static final String TAG = "FCMService";
|
||
|
|
private static final String CHANNEL_ID = "kbs_calendar_channel";
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onNewToken(@NonNull String token) {
|
||
|
|
super.onNewToken(token);
|
||
|
|
AuthRepository.updateDeviceToken(token);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
|
||
|
|
super.onMessageReceived(remoteMessage);
|
||
|
|
|
||
|
|
// 1. DATA PAYLOAD (Bakgrunnsoppdatering)
|
||
|
|
if (remoteMessage.getData().size() > 0) {
|
||
|
|
String forceRefresh = remoteMessage.getData().get("force_refresh");
|
||
|
|
String type = remoteMessage.getData().get("type"); // "tasks_update", "news_update", "calendar_update"
|
||
|
|
|
||
|
|
if ("true".equalsIgnoreCase(forceRefresh)) {
|
||
|
|
Log.d(TAG, "Mottok force_refresh. Type: " + type);
|
||
|
|
|
||
|
|
// Uansett hva det er, ugyldiggjør cachen slik at neste gang brukeren åpner appen, hentes ferske data.
|
||
|
|
if ("news_update".equals(type)) {
|
||
|
|
CacheManager.invalidateCache(getApplicationContext(), "news");
|
||
|
|
} else if ("tasks_update".equals(type)) {
|
||
|
|
CacheManager.invalidateCache(getApplicationContext(), "tasks");
|
||
|
|
updateTasksInBackground(); // Hent oppgaver med en gang i bakgrunnen
|
||
|
|
} else {
|
||
|
|
// Kalender eller generelt
|
||
|
|
CacheManager.invalidateCache(getApplicationContext(), "calendar");
|
||
|
|
updateCalendarAndAlarms();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. NOTIFICATION PAYLOAD (Synlig varsel - Nyheter etc)
|
||
|
|
// Android viser disse automatisk når appen er i bakgrunnen.
|
||
|
|
// Denne koden kjører hvis appen er i forgrunnen, eller hvis meldingen kun er "Data" og vi vil lage varsel manuelt.
|
||
|
|
if (remoteMessage.getNotification() != null) {
|
||
|
|
showNotification(
|
||
|
|
remoteMessage.getNotification().getTitle(),
|
||
|
|
remoteMessage.getNotification().getBody()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateCalendarAndAlarms() {
|
||
|
|
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
CacheManager.saveCalendarEvents(getApplicationContext(), response.body());
|
||
|
|
AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateTasksInBackground() {
|
||
|
|
// Henter oppgaver stille i bakgrunnen slik at de er klare når brukeren åpner appen
|
||
|
|
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> response) {
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
CacheManager.saveTasks(getApplicationContext(), response.body());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override public void onFailure(Call<List<TaskItem>> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void showNotification(String title, String message) {
|
||
|
|
createNotificationChannel();
|
||
|
|
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
|
|
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Intent tapIntent = new Intent(this, MainActivity.class);
|
||
|
|
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||
|
|
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||
|
|
this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE
|
||
|
|
);
|
||
|
|
|
||
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||
|
|
.setSmallIcon(R.drawable.ic_stat_kbs)
|
||
|
|
.setColor(ContextCompat.getColor(this, R.color.kbs_logo_blue))
|
||
|
|
.setContentTitle(title)
|
||
|
|
.setContentText(message)
|
||
|
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||
|
|
.setContentIntent(pendingIntent)
|
||
|
|
.setAutoCancel(true);
|
||
|
|
|
||
|
|
NotificationManagerCompat.from(this).notify((int) System.currentTimeMillis(), builder.build());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void createNotificationChannel() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
|
NotificationChannel channel = new NotificationChannel(
|
||
|
|
CHANNEL_ID, "KBS Varsler", NotificationManager.IMPORTANCE_HIGH
|
||
|
|
);
|
||
|
|
getSystemService(NotificationManager.class).createNotificationChannel(channel);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsAdapter.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.ImageView;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import com.bumptech.glide.Glide;
|
||
|
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class NewsAdapter extends RecyclerView.Adapter<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.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<>();
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
return inflater.inflate(R.layout.fragment_news_full, container, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||
|
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
|
||
|
|
recyclerViewNews = view.findViewById(R.id.recycler_news_full);
|
||
|
|
recyclerViewCategories = view.findViewById(R.id.recycler_categories);
|
||
|
|
progressBar = view.findViewById(R.id.loading_news_full);
|
||
|
|
|
||
|
|
// Setup Nyhetsliste
|
||
|
|
recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext()));
|
||
|
|
|
||
|
|
// Setup Kategorier (Horisontal)
|
||
|
|
recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||
|
|
setupCategories();
|
||
|
|
|
||
|
|
fetchAllNews();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void setupCategories() {
|
||
|
|
List<String> categories = Arrays.asList(
|
||
|
|
"Alle",
|
||
|
|
"Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel",
|
||
|
|
"Ferieavvikling", "Fest og moro", "Generell drift",
|
||
|
|
"HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX"
|
||
|
|
);
|
||
|
|
|
||
|
|
CategoryAdapter catAdapter = new CategoryAdapter(categories, selectedCategory -> {
|
||
|
|
filterNews(selectedCategory);
|
||
|
|
});
|
||
|
|
recyclerViewCategories.setAdapter(catAdapter);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchAllNews() {
|
||
|
|
progressBar.setVisibility(View.VISIBLE);
|
||
|
|
RetrofitClient.getApiService().getAllPosts().enqueue(new Callback<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);
|
||
|
|
|
||
|
|
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) {
|
||
|
|
if (post.getCategoryName().equals(category)) {
|
||
|
|
filteredList.add(post);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
newsAdapter.updateList(filteredList);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void formatDates(List<WpPost> posts) {
|
||
|
|
SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||
|
|
SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
|
||
|
|
|
||
|
|
for (WpPost post : posts) {
|
||
|
|
try {
|
||
|
|
Date date = rawFormat.parse(post.date);
|
||
|
|
post.date = targetFormat.format(date);
|
||
|
|
} catch (Exception e) {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsItem.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
public class NewsItem {
|
||
|
|
private String title;
|
||
|
|
private String excerpt; // Kort tekst/ingress
|
||
|
|
private String author;
|
||
|
|
|
||
|
|
public NewsItem(String title, String excerpt, String author) {
|
||
|
|
this.title = title;
|
||
|
|
this.excerpt = excerpt;
|
||
|
|
this.author = author;
|
||
|
|
}
|
||
|
|
|
||
|
|
public String getTitle() { return title; }
|
||
|
|
public String getExcerpt() { return excerpt; }
|
||
|
|
public String getAuthor() { return author; }
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationHelper.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.Manifest;
|
||
|
|
import android.app.PendingIntent;
|
||
|
|
import android.content.Context;
|
||
|
|
import android.content.Intent;
|
||
|
|
import android.content.pm.PackageManager;
|
||
|
|
import android.os.Build;
|
||
|
|
import androidx.core.app.ActivityCompat;
|
||
|
|
import androidx.core.app.NotificationCompat;
|
||
|
|
import androidx.core.app.NotificationManagerCompat;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
|
||
|
|
public class NotificationHelper {
|
||
|
|
|
||
|
|
private static final String CHANNEL_ID = "kbs_calendar_channel";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* En enkel metode for å vise et standard KBS-varsel.
|
||
|
|
*/
|
||
|
|
public static void showNotification(Context context, String title, String message) {
|
||
|
|
// Sjekk tillatelse for Android 13+
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
|
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Intent for å åpne appen når man trykker på varselet
|
||
|
|
Intent intent = new Intent(context, MainActivity.class);
|
||
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||
|
|
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||
|
|
context,
|
||
|
|
(int) System.currentTimeMillis(),
|
||
|
|
intent,
|
||
|
|
PendingIntent.FLAG_IMMUTABLE
|
||
|
|
);
|
||
|
|
|
||
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||
|
|
.setSmallIcon(R.drawable.ic_stat_kbs) // Bruker samme ikon som kalender
|
||
|
|
.setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue))
|
||
|
|
.setContentTitle(title)
|
||
|
|
.setContentText(message)
|
||
|
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||
|
|
.setContentIntent(pendingIntent)
|
||
|
|
.setAutoCancel(true);
|
||
|
|
|
||
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||
|
|
notificationManager.notify((int) System.currentTimeMillis(), builder.build());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\ProfileFragment.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.Button;
|
||
|
|
import android.widget.ImageView;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import android.widget.Toast;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.fragment.app.Fragment;
|
||
|
|
import androidx.navigation.NavController;
|
||
|
|
import androidx.navigation.Navigation;
|
||
|
|
|
||
|
|
import com.bumptech.glide.Glide;
|
||
|
|
import com.bumptech.glide.request.RequestOptions;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignIn;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
|
||
|
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
|
||
|
|
|
||
|
|
public class ProfileFragment extends Fragment {
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
View view = inflater.inflate(R.layout.fragment_profile, container, false);
|
||
|
|
|
||
|
|
ImageView closeBtn = view.findViewById(R.id.btn_close_profile);
|
||
|
|
ImageView profileImage = view.findViewById(R.id.profile_image);
|
||
|
|
TextView nameText = view.findViewById(R.id.profile_name);
|
||
|
|
TextView emailText = view.findViewById(R.id.profile_email);
|
||
|
|
TextView roleText = view.findViewById(R.id.profile_role);
|
||
|
|
Button logoutBtn = view.findViewById(R.id.btn_logout);
|
||
|
|
Button updateInfoBtn = view.findViewById(R.id.btn_update_info);
|
||
|
|
TextView versionText = view.findViewById(R.id.tv_version_info); // NYTT
|
||
|
|
|
||
|
|
UserManager user = UserManager.getInstance();
|
||
|
|
nameText.setText(user.getUserDisplayName());
|
||
|
|
emailText.setText(user.getUserEmail());
|
||
|
|
roleText.setText("Rolle: " + user.getUserRole());
|
||
|
|
|
||
|
|
// NYTT: Sett versjonstekst
|
||
|
|
String versionInfo = "Versjon " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")";
|
||
|
|
versionText.setText(versionInfo);
|
||
|
|
|
||
|
|
if (user.getPhotoUrl() != null) {
|
||
|
|
Glide.with(this)
|
||
|
|
.load(user.getPhotoUrl())
|
||
|
|
.apply(RequestOptions.circleCropTransform())
|
||
|
|
.into(profileImage);
|
||
|
|
}
|
||
|
|
|
||
|
|
closeBtn.setOnClickListener(v -> {
|
||
|
|
Navigation.findNavController(view).navigateUp();
|
||
|
|
});
|
||
|
|
|
||
|
|
updateInfoBtn.setOnClickListener(v -> {
|
||
|
|
Bundle bundle = new Bundle();
|
||
|
|
bundle.putInt("formId", 1);
|
||
|
|
Navigation.findNavController(view).navigate(R.id.action_profile_to_form, bundle);
|
||
|
|
});
|
||
|
|
|
||
|
|
logoutBtn.setOnClickListener(v -> performLogout());
|
||
|
|
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void performLogout() {
|
||
|
|
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||
|
|
.requestIdToken(MainActivity.GOOGLE_WEB_CLIENT_ID)
|
||
|
|
.requestEmail()
|
||
|
|
.build();
|
||
|
|
GoogleSignInClient client = GoogleSignIn.getClient(requireActivity(), gso);
|
||
|
|
|
||
|
|
client.signOut().addOnCompleteListener(task -> {
|
||
|
|
UserManager.getInstance().logout();
|
||
|
|
RetrofitClient.clearClient();
|
||
|
|
NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment);
|
||
|
|
navController.navigate(R.id.action_profile_to_login);
|
||
|
|
Toast.makeText(getContext(), "Du er nå logget ut", Toast.LENGTH_SHORT).show();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\RegisterDeviceRequest.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import com.google.gson.annotations.SerializedName;
|
||
|
|
|
||
|
|
public class RegisterDeviceRequest {
|
||
|
|
@SerializedName("fcm_token")
|
||
|
|
public String fcmToken;
|
||
|
|
|
||
|
|
@SerializedName("platform")
|
||
|
|
public String platform;
|
||
|
|
|
||
|
|
public RegisterDeviceRequest(String fcmToken) {
|
||
|
|
this.fcmToken = fcmToken;
|
||
|
|
this.platform = "android";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\RetrofitClient.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import com.google.gson.Gson;
|
||
|
|
import com.google.gson.GsonBuilder;
|
||
|
|
import com.google.gson.reflect.TypeToken;
|
||
|
|
|
||
|
|
import java.io.IOException;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
import okhttp3.Interceptor;
|
||
|
|
import okhttp3.OkHttpClient;
|
||
|
|
import okhttp3.Request;
|
||
|
|
import okhttp3.Response;
|
||
|
|
import okhttp3.logging.HttpLoggingInterceptor;
|
||
|
|
import retrofit2.Retrofit;
|
||
|
|
import retrofit2.converter.gson.GsonConverterFactory;
|
||
|
|
|
||
|
|
public class RetrofitClient {
|
||
|
|
private static final String BASE_URL = "https://intranet.kbs.no/";
|
||
|
|
private static Retrofit retrofit = null;
|
||
|
|
public static WordPressApiService getApiService() {
|
||
|
|
if (retrofit == null) {
|
||
|
|
|
||
|
|
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||
|
|
if (BuildConfig.DEBUG) {
|
||
|
|
// I debug-modus logger vi det mest nødvendige
|
||
|
|
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
|
||
|
|
} else {
|
||
|
|
// I release er vi stille for ytelse og sikkerhet
|
||
|
|
logging.setLevel(HttpLoggingInterceptor.Level.NONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
OkHttpClient client = new OkHttpClient.Builder()
|
||
|
|
.addInterceptor(logging)
|
||
|
|
.addInterceptor(new Interceptor() {
|
||
|
|
@Override
|
||
|
|
public Response intercept(Chain chain) throws IOException {
|
||
|
|
Request originalRequest = chain.request();
|
||
|
|
Request.Builder builder = originalRequest.newBuilder();
|
||
|
|
|
||
|
|
String dynamicCookie = UserManager.getInstance().getCookie();
|
||
|
|
if (dynamicCookie != null && !dynamicCookie.isEmpty()) {
|
||
|
|
builder.header("Cookie", dynamicCookie);
|
||
|
|
}
|
||
|
|
|
||
|
|
return chain.proceed(builder.build());
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.build();
|
||
|
|
|
||
|
|
Gson gson = new GsonBuilder()
|
||
|
|
.registerTypeAdapter(new TypeToken<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\TaskAdapter.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.graphics.Color;
|
||
|
|
import android.graphics.Paint;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.CheckBox;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.cardview.widget.CardView;
|
||
|
|
import androidx.core.content.ContextCompat;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
|
||
|
|
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
|
||
|
|
|
||
|
|
private List<TaskItem> tasks;
|
||
|
|
private OnTaskClickListener listener;
|
||
|
|
private String currentUserEmail;
|
||
|
|
|
||
|
|
public interface OnTaskClickListener {
|
||
|
|
void onTaskClick(TaskItem task);
|
||
|
|
void onStatusChanged(TaskItem task, boolean isDone);
|
||
|
|
}
|
||
|
|
|
||
|
|
public TaskAdapter(List<TaskItem> tasks, OnTaskClickListener listener) {
|
||
|
|
this.tasks = tasks;
|
||
|
|
this.listener = listener;
|
||
|
|
this.currentUserEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
}
|
||
|
|
|
||
|
|
@NonNull
|
||
|
|
@Override
|
||
|
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||
|
|
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_task, parent, false);
|
||
|
|
return new ViewHolder(v);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||
|
|
TaskItem task = tasks.get(position);
|
||
|
|
holder.title.setText(task.getTitle());
|
||
|
|
|
||
|
|
if (task.getDueDate() > 0) {
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault());
|
||
|
|
holder.date.setText("Frist: " + sdf.format(new Date(task.getDueDate())));
|
||
|
|
} else {
|
||
|
|
holder.date.setText("Ingen frist");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (task.getCreatedByEmail() != null && !task.getCreatedByEmail().equalsIgnoreCase(currentUserEmail)) {
|
||
|
|
holder.creator.setText("Tildelt av: " + task.getCreatedByName());
|
||
|
|
holder.creator.setVisibility(View.VISIBLE);
|
||
|
|
} else {
|
||
|
|
holder.creator.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// SJEKK: Er jeg en deltaker?
|
||
|
|
boolean isParticipant = task.isUserParticipant(currentUserEmail);
|
||
|
|
|
||
|
|
if (isParticipant) {
|
||
|
|
// Hvis jeg er deltaker: Vis checkbox og la meg endre MIN status
|
||
|
|
holder.checkBox.setVisibility(View.VISIBLE);
|
||
|
|
boolean myStatus = task.getParticipantStatus(currentUserEmail);
|
||
|
|
holder.checkBox.setOnCheckedChangeListener(null); // Hindre trigging ved resirkulering
|
||
|
|
holder.checkBox.setChecked(myStatus);
|
||
|
|
holder.checkBox.setOnClickListener(v -> listener.onStatusChanged(task, holder.checkBox.isChecked()));
|
||
|
|
} else {
|
||
|
|
// Hvis jeg IKKE er deltaker (f.eks. Admin som ser på "Alle"):
|
||
|
|
// Skjul checkboxen i listen. Admin må åpne detaljer for å endre andres status.
|
||
|
|
holder.checkBox.setVisibility(View.INVISIBLE);
|
||
|
|
holder.checkBox.setOnClickListener(null);
|
||
|
|
}
|
||
|
|
|
||
|
|
long now = System.currentTimeMillis();
|
||
|
|
// Sjekk overtid (kun hvis frist er satt)
|
||
|
|
// Er jeg ikke deltaker, sjekker vi om hele oppgaven er ferdig
|
||
|
|
boolean isDoneToCheck = isParticipant ? task.getParticipantStatus(currentUserEmail) : task.isFullyCompleted();
|
||
|
|
boolean isOverdue = task.getDueDate() > 0 && task.getDueDate() < now && !task.isFullyCompleted() && !isDoneToCheck;
|
||
|
|
|
||
|
|
if (isOverdue) {
|
||
|
|
holder.cardView.setCardBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_soft_light_pink_beige));
|
||
|
|
holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_logo_accent_red));
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("dd. MMM", Locale.getDefault());
|
||
|
|
holder.date.setText("FORFALT: " + sdf.format(new Date(task.getDueDate())));
|
||
|
|
} else {
|
||
|
|
holder.cardView.setCardBackgroundColor(Color.WHITE);
|
||
|
|
holder.date.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.kbs_muted_blue_gray));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isDoneToCheck || task.isFullyCompleted()) {
|
||
|
|
holder.title.setPaintFlags(holder.title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||
|
|
holder.title.setTextColor(Color.GRAY);
|
||
|
|
holder.cardView.setCardBackgroundColor(Color.parseColor("#F5F5F5"));
|
||
|
|
} else if (!isOverdue) {
|
||
|
|
holder.title.setPaintFlags(holder.title.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
|
||
|
|
holder.title.setTextColor(Color.BLACK);
|
||
|
|
}
|
||
|
|
|
||
|
|
int total = task.getAssigneeStatus().size();
|
||
|
|
int done = 0;
|
||
|
|
for (Boolean b : task.getAssigneeStatus().values()) if (b) done++;
|
||
|
|
holder.progress.setText("Fremdrift: " + done + "/" + total);
|
||
|
|
|
||
|
|
holder.itemView.setOnClickListener(v -> listener.onTaskClick(task));
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int getItemCount() { return tasks.size(); }
|
||
|
|
|
||
|
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||
|
|
TextView title, date, creator, progress;
|
||
|
|
CheckBox checkBox;
|
||
|
|
CardView cardView;
|
||
|
|
ViewHolder(View v) {
|
||
|
|
super(v);
|
||
|
|
title = v.findViewById(R.id.task_title);
|
||
|
|
date = v.findViewById(R.id.task_date);
|
||
|
|
creator = v.findViewById(R.id.task_creator);
|
||
|
|
progress = v.findViewById(R.id.task_progress);
|
||
|
|
checkBox = v.findViewById(R.id.task_checkbox);
|
||
|
|
cardView = (CardView) v;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskDetailsBottomSheet.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.Button;
|
||
|
|
import android.widget.CheckBox;
|
||
|
|
import android.widget.LinearLayout;
|
||
|
|
import android.widget.TextView;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||
|
|
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||
|
|
import java.text.SimpleDateFormat;
|
||
|
|
import java.util.Date;
|
||
|
|
import java.util.Locale;
|
||
|
|
import java.util.Map;
|
||
|
|
|
||
|
|
public class TaskDetailsBottomSheet extends BottomSheetDialogFragment {
|
||
|
|
|
||
|
|
private TaskItem task;
|
||
|
|
private OnTaskChangeListener listener;
|
||
|
|
|
||
|
|
public interface OnTaskChangeListener {
|
||
|
|
void onTaskChanged();
|
||
|
|
void onTaskDeleted(TaskItem task);
|
||
|
|
void onEditRequested(TaskItem task);
|
||
|
|
}
|
||
|
|
|
||
|
|
public TaskDetailsBottomSheet(TaskItem task, OnTaskChangeListener listener) {
|
||
|
|
this.task = task;
|
||
|
|
this.listener = listener;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
View v = inflater.inflate(R.layout.bottom_sheet_task_details, container, false);
|
||
|
|
|
||
|
|
TextView title = v.findViewById(R.id.detail_task_title);
|
||
|
|
TextView date = v.findViewById(R.id.detail_task_date);
|
||
|
|
TextView desc = v.findViewById(R.id.detail_task_desc);
|
||
|
|
LinearLayout participantsContainer = v.findViewById(R.id.container_participants_status);
|
||
|
|
SwitchMaterial switchNotify = v.findViewById(R.id.switch_notifications);
|
||
|
|
LinearLayout ownerActions = v.findViewById(R.id.layout_owner_actions);
|
||
|
|
Button btnDelete = v.findViewById(R.id.btn_delete_task);
|
||
|
|
Button btnEdit = v.findViewById(R.id.btn_edit_task);
|
||
|
|
Button btnClose = v.findViewById(R.id.btn_close_details); // NYTT
|
||
|
|
|
||
|
|
title.setText(task.getTitle());
|
||
|
|
if (task.getDueDate() > 0) {
|
||
|
|
SimpleDateFormat sdf = new SimpleDateFormat("EEEE dd. MMMM yyyy", Locale.getDefault());
|
||
|
|
date.setText("Frist: " + sdf.format(new Date(task.getDueDate())));
|
||
|
|
} else {
|
||
|
|
date.setText("Ingen frist");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (task.getDescription() != null && !task.getDescription().isEmpty()) {
|
||
|
|
desc.setText(task.getDescription());
|
||
|
|
desc.setVisibility(View.VISIBLE);
|
||
|
|
}
|
||
|
|
|
||
|
|
participantsContainer.removeAllViews();
|
||
|
|
UserManager userManager = UserManager.getInstance();
|
||
|
|
String myEmail = userManager.getUserEmail();
|
||
|
|
|
||
|
|
boolean canManageOthers = task.getCreatedByEmail().equalsIgnoreCase(myEmail) || userManager.isAdmin();
|
||
|
|
|
||
|
|
for (Map.Entry<String, Boolean> entry : task.getAssigneeStatus().entrySet()) {
|
||
|
|
String email = entry.getKey();
|
||
|
|
boolean isDone = entry.getValue();
|
||
|
|
|
||
|
|
if (canManageOthers) {
|
||
|
|
CheckBox cb = new CheckBox(getContext());
|
||
|
|
cb.setText(email + (isDone ? " (Fullført)" : ""));
|
||
|
|
cb.setChecked(isDone);
|
||
|
|
|
||
|
|
if (isDone) cb.setTextColor(getResources().getColor(android.R.color.darker_gray));
|
||
|
|
else cb.setTextColor(getResources().getColor(R.color.black));
|
||
|
|
|
||
|
|
cb.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||
|
|
task.setParticipantStatus(email, isChecked);
|
||
|
|
|
||
|
|
cb.setText(email + (isChecked ? " (Fullført)" : ""));
|
||
|
|
if (isChecked) cb.setTextColor(getResources().getColor(android.R.color.darker_gray));
|
||
|
|
else cb.setTextColor(getResources().getColor(R.color.black));
|
||
|
|
|
||
|
|
if (listener != null) listener.onTaskChanged();
|
||
|
|
|
||
|
|
// Sjekk om alle er ferdige, i så fall lukk vinduet
|
||
|
|
if (isChecked && areAllParticipantsFinished()) {
|
||
|
|
dismiss();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
participantsContainer.addView(cb);
|
||
|
|
|
||
|
|
} else {
|
||
|
|
TextView t = new TextView(getContext());
|
||
|
|
String status = isDone ? "✅ Fullført" : "⏳ Pågår";
|
||
|
|
t.setText("• " + email + ": " + status);
|
||
|
|
t.setPadding(0, 8, 0, 8);
|
||
|
|
t.setTextSize(14);
|
||
|
|
participantsContainer.addView(t);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
switchNotify.setChecked(task.isNotificationsEnabled());
|
||
|
|
switchNotify.setOnCheckedChangeListener((btn, isChecked) -> {
|
||
|
|
task.setNotificationsEnabled(isChecked);
|
||
|
|
if (listener != null) listener.onTaskChanged();
|
||
|
|
});
|
||
|
|
|
||
|
|
if (canManageOthers) {
|
||
|
|
ownerActions.setVisibility(View.VISIBLE);
|
||
|
|
btnDelete.setOnClickListener(view -> {
|
||
|
|
if (listener != null) listener.onTaskDeleted(task);
|
||
|
|
dismiss();
|
||
|
|
});
|
||
|
|
btnEdit.setOnClickListener(view -> {
|
||
|
|
if (listener != null) listener.onEditRequested(task);
|
||
|
|
dismiss();
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
ownerActions.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// NYTT: Lukk-knapp funksjonalitet
|
||
|
|
btnClose.setOnClickListener(view -> dismiss());
|
||
|
|
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
|
||
|
|
private boolean areAllParticipantsFinished() {
|
||
|
|
for (Boolean isDone : task.getAssigneeStatus().values()) {
|
||
|
|
if (!isDone) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskItem.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import java.io.Serializable;
|
||
|
|
import java.util.HashMap;
|
||
|
|
import java.util.Map;
|
||
|
|
import java.util.UUID;
|
||
|
|
|
||
|
|
public class TaskItem implements Serializable {
|
||
|
|
private String id;
|
||
|
|
private String title;
|
||
|
|
private String description;
|
||
|
|
private long dueDate;
|
||
|
|
private String createdByEmail;
|
||
|
|
private String createdByName;
|
||
|
|
|
||
|
|
private Map<String, Boolean> assigneeStatus = new HashMap<>();
|
||
|
|
|
||
|
|
private boolean notificationsEnabled = true;
|
||
|
|
private boolean isFullyCompleted = false;
|
||
|
|
|
||
|
|
public TaskItem(String title, String description, long dueDate) {
|
||
|
|
this.id = UUID.randomUUID().toString();
|
||
|
|
this.title = title;
|
||
|
|
this.description = description;
|
||
|
|
this.dueDate = dueDate;
|
||
|
|
this.createdByEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
this.createdByName = UserManager.getInstance().getUserDisplayName();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Getters
|
||
|
|
public String getId() { return id; }
|
||
|
|
public String getTitle() { return title; }
|
||
|
|
public String getDescription() { return description; }
|
||
|
|
public long getDueDate() { return dueDate; }
|
||
|
|
public String getCreatedByEmail() { return createdByEmail; }
|
||
|
|
public String getCreatedByName() { return createdByName; }
|
||
|
|
public boolean isNotificationsEnabled() { return notificationsEnabled; }
|
||
|
|
public boolean isFullyCompleted() { return isFullyCompleted; }
|
||
|
|
|
||
|
|
// Setters (NYTT FOR REDIGERING)
|
||
|
|
public void setTitle(String title) { this.title = title; }
|
||
|
|
public void setDescription(String description) { this.description = description; }
|
||
|
|
public void setDueDate(long dueDate) { this.dueDate = dueDate; }
|
||
|
|
public void setNotificationsEnabled(boolean enabled) { this.notificationsEnabled = enabled; }
|
||
|
|
public void setFullyCompleted(boolean fullyCompleted) { this.isFullyCompleted = fullyCompleted; }
|
||
|
|
|
||
|
|
// Participant logikk
|
||
|
|
public void addAssignee(String email) { assigneeStatus.put(email, false); }
|
||
|
|
public void setParticipantStatus(String email, boolean done) { assigneeStatus.put(email, done); }
|
||
|
|
public boolean getParticipantStatus(String email) { return assigneeStatus.getOrDefault(email, false); }
|
||
|
|
public boolean isUserParticipant(String email) { return assigneeStatus.containsKey(email); }
|
||
|
|
public Map<String, Boolean> getAssigneeStatus() { return assigneeStatus; }
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\TaskReminderWorker.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.content.Context;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.work.Worker;
|
||
|
|
import androidx.work.WorkerParameters;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class TaskReminderWorker extends Worker {
|
||
|
|
|
||
|
|
public TaskReminderWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||
|
|
super(context, params);
|
||
|
|
}
|
||
|
|
|
||
|
|
@NonNull
|
||
|
|
@Override
|
||
|
|
public Result doWork() {
|
||
|
|
Context context = getApplicationContext();
|
||
|
|
List<TaskItem> tasks = CacheManager.getTasks(context);
|
||
|
|
String myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
long now = System.currentTimeMillis();
|
||
|
|
|
||
|
|
for (TaskItem task : tasks) {
|
||
|
|
if (!task.isNotificationsEnabled() || task.isFullyCompleted() || task.getParticipantStatus(myEmail)) continue;
|
||
|
|
|
||
|
|
long diff = task.getDueDate() - now;
|
||
|
|
|
||
|
|
// Varsle hvis fristen er i dag (innenfor 24 timer)
|
||
|
|
if (diff > 0 && diff < 86400000L) {
|
||
|
|
NotificationHelper.showNotification(context, "Frist i dag!", "Oppgaven '" + task.getTitle() + "' forfaller snart.");
|
||
|
|
}
|
||
|
|
// Varsle hvis over frist (hver gang worker kjører, f.eks annenhver dag)
|
||
|
|
else if (diff < 0) {
|
||
|
|
NotificationHelper.showNotification(context, "Over frist!", "Oppgaven '" + task.getTitle() + "' er forsinket.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Result.success();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\TasksFragment.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import android.os.Bundle;
|
||
|
|
import android.view.LayoutInflater;
|
||
|
|
import android.view.View;
|
||
|
|
import android.view.ViewGroup;
|
||
|
|
import android.widget.CheckBox;
|
||
|
|
import android.widget.Toast;
|
||
|
|
import androidx.annotation.NonNull;
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import androidx.fragment.app.Fragment;
|
||
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||
|
|
import androidx.recyclerview.widget.RecyclerView;
|
||
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||
|
|
import com.google.android.material.tabs.TabLayout;
|
||
|
|
import com.google.gson.JsonElement;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.Collections;
|
||
|
|
import java.util.List;
|
||
|
|
import retrofit2.Call;
|
||
|
|
import retrofit2.Callback;
|
||
|
|
import retrofit2.Response;
|
||
|
|
|
||
|
|
public class TasksFragment extends Fragment implements TaskAdapter.OnTaskClickListener {
|
||
|
|
|
||
|
|
private RecyclerView recyclerView;
|
||
|
|
private TaskAdapter adapter;
|
||
|
|
private TabLayout tabLayout;
|
||
|
|
private SwipeRefreshLayout swipeRefresh;
|
||
|
|
private CheckBox cbShowCompleted;
|
||
|
|
private List<TaskItem> allTasks = new ArrayList<>();
|
||
|
|
private String myEmail;
|
||
|
|
|
||
|
|
@Nullable
|
||
|
|
@Override
|
||
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||
|
|
return inflater.inflate(R.layout.fragment_tasks, container, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||
|
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
|
||
|
|
myEmail = UserManager.getInstance().getUserEmail();
|
||
|
|
tabLayout = view.findViewById(R.id.task_tabs);
|
||
|
|
recyclerView = view.findViewById(R.id.recycler_tasks);
|
||
|
|
swipeRefresh = view.findViewById(R.id.swipe_refresh_tasks);
|
||
|
|
cbShowCompleted = view.findViewById(R.id.cb_show_completed);
|
||
|
|
|
||
|
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||
|
|
|
||
|
|
setupTabs();
|
||
|
|
|
||
|
|
FloatingActionButton fab = view.findViewById(R.id.fab_add_task);
|
||
|
|
fab.setOnClickListener(v -> {
|
||
|
|
AddTaskBottomSheet dialog = new AddTaskBottomSheet();
|
||
|
|
dialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() {
|
||
|
|
@Override
|
||
|
|
public void onTaskAdded(TaskItem task) {
|
||
|
|
allTasks.add(0, task);
|
||
|
|
saveAndSync();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onTaskUpdated(TaskItem task) {
|
||
|
|
saveAndSync();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
dialog.show(getChildFragmentManager(), "AddTask");
|
||
|
|
});
|
||
|
|
|
||
|
|
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||
|
|
@Override public void onTabSelected(TabLayout.Tab tab) {
|
||
|
|
updateFilterUI(tab.getPosition());
|
||
|
|
filterAndDisplay();
|
||
|
|
}
|
||
|
|
@Override public void onTabUnselected(TabLayout.Tab tab) {}
|
||
|
|
@Override public void onTabReselected(TabLayout.Tab tab) {}
|
||
|
|
});
|
||
|
|
|
||
|
|
cbShowCompleted.setOnCheckedChangeListener((buttonView, isChecked) -> filterAndDisplay());
|
||
|
|
|
||
|
|
swipeRefresh.setOnRefreshListener(this::fetchTasksFromServer);
|
||
|
|
|
||
|
|
allTasks = CacheManager.getTasks(getContext());
|
||
|
|
updateFilterUI(tabLayout.getSelectedTabPosition());
|
||
|
|
filterAndDisplay();
|
||
|
|
fetchTasksFromServer();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void setupTabs() {
|
||
|
|
tabLayout.removeAllTabs();
|
||
|
|
tabLayout.addTab(tabLayout.newTab().setText("Mine"));
|
||
|
|
tabLayout.addTab(tabLayout.newTab().setText("Fullførte"));
|
||
|
|
tabLayout.addTab(tabLayout.newTab().setText("Tildelt andre"));
|
||
|
|
if (UserManager.getInstance().isEditorOrAbove()) {
|
||
|
|
tabLayout.addTab(tabLayout.newTab().setText("Alle"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateFilterUI(int tabIndex) {
|
||
|
|
if (tabIndex == 3) {
|
||
|
|
cbShowCompleted.setVisibility(View.VISIBLE);
|
||
|
|
} else {
|
||
|
|
cbShowCompleted.setVisibility(View.GONE);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void fetchTasksFromServer() {
|
||
|
|
swipeRefresh.setRefreshing(true);
|
||
|
|
RetrofitClient.getApiService().getTasks().enqueue(new Callback<List<TaskItem>>() {
|
||
|
|
@Override
|
||
|
|
public void onResponse(Call<List<TaskItem>> call, Response<List<TaskItem>> response) {
|
||
|
|
swipeRefresh.setRefreshing(false);
|
||
|
|
if (response.isSuccessful() && response.body() != null) {
|
||
|
|
allTasks = response.body();
|
||
|
|
CacheManager.saveTasks(getContext(), allTasks);
|
||
|
|
filterAndDisplay();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public void onFailure(Call<List<TaskItem>> call, Throwable t) {
|
||
|
|
swipeRefresh.setRefreshing(false);
|
||
|
|
Toast.makeText(getContext(), "Kunne ikke synkronisere oppgaver", Toast.LENGTH_SHORT).show();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private void filterAndDisplay() {
|
||
|
|
List<TaskItem> filtered = new ArrayList<>();
|
||
|
|
int selectedTab = tabLayout.getSelectedTabPosition();
|
||
|
|
|
||
|
|
for (TaskItem t : allTasks) {
|
||
|
|
boolean isParticipant = t.isUserParticipant(myEmail);
|
||
|
|
boolean isCreator = t.getCreatedByEmail().equalsIgnoreCase(myEmail);
|
||
|
|
boolean iHaveDoneIt = t.getParticipantStatus(myEmail);
|
||
|
|
boolean hasOtherParticipants = false;
|
||
|
|
for (String email : t.getAssigneeStatus().keySet()) {
|
||
|
|
if (!email.equalsIgnoreCase(myEmail)) { hasOtherParticipants = true; break; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// NYTT: Sjekk dynamisk om ALLE deltakerne har fullført oppgaven
|
||
|
|
boolean allParticipantsFinished = true;
|
||
|
|
if (t.getAssigneeStatus().isEmpty()) {
|
||
|
|
allParticipantsFinished = false;
|
||
|
|
} else {
|
||
|
|
for (Boolean isDone : t.getAssigneeStatus().values()) {
|
||
|
|
if (!isDone) {
|
||
|
|
allParticipantsFinished = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// En oppgave er "helt ferdig" hvis flagget er satt ELLER alle har krysset av
|
||
|
|
boolean effectivelyFinished = t.isFullyCompleted() || allParticipantsFinished;
|
||
|
|
|
||
|
|
// Oppdater objektet slik at det lagres riktig neste gang (valgfritt men lurt)
|
||
|
|
if (effectivelyFinished && !t.isFullyCompleted()) {
|
||
|
|
t.setFullyCompleted(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (selectedTab) {
|
||
|
|
case 0: // MINE
|
||
|
|
if (isParticipant && !iHaveDoneIt && !t.isFullyCompleted()) filtered.add(t);
|
||
|
|
break;
|
||
|
|
case 1: // FULLFØRTE
|
||
|
|
if (t.isFullyCompleted() || (isParticipant && iHaveDoneIt)) filtered.add(t);
|
||
|
|
break;
|
||
|
|
case 2: // TILDELT ANDRE
|
||
|
|
if (isCreator && !t.isFullyCompleted() && hasOtherParticipants) filtered.add(t);
|
||
|
|
break;
|
||
|
|
case 3: // ALLE (Admin)
|
||
|
|
if (cbShowCompleted.isChecked()) {
|
||
|
|
// Vis alt
|
||
|
|
filtered.add(t);
|
||
|
|
} else {
|
||
|
|
// Vis kun hvis IKKE ferdig
|
||
|
|
if (!effectivelyFinished) filtered.add(t);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// SORTERING
|
||
|
|
Collections.sort(filtered, (t1, t2) -> {
|
||
|
|
boolean t1HasDate = t1.getDueDate() > 0;
|
||
|
|
boolean t2HasDate = t2.getDueDate() > 0;
|
||
|
|
|
||
|
|
if (selectedTab == 1) {
|
||
|
|
if (t1HasDate && !t2HasDate) return -1;
|
||
|
|
if (!t1HasDate && t2HasDate) return 1;
|
||
|
|
if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle());
|
||
|
|
return Long.compare(t2.getDueDate(), t1.getDueDate());
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
if (t1HasDate && !t2HasDate) return -1;
|
||
|
|
if (!t1HasDate && t2HasDate) return 1;
|
||
|
|
if (!t1HasDate && !t2HasDate) return t1.getTitle().compareToIgnoreCase(t2.getTitle());
|
||
|
|
return Long.compare(t1.getDueDate(), t2.getDueDate());
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
adapter = new TaskAdapter(filtered, this);
|
||
|
|
recyclerView.setAdapter(adapter);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onTaskClick(TaskItem task) {
|
||
|
|
TaskDetailsBottomSheet sheet = new TaskDetailsBottomSheet(task, new TaskDetailsBottomSheet.OnTaskChangeListener() {
|
||
|
|
@Override public void onTaskChanged() { saveAndSync(); }
|
||
|
|
@Override public void onTaskDeleted(TaskItem taskToDelete) {
|
||
|
|
allTasks.remove(taskToDelete);
|
||
|
|
saveAndSync();
|
||
|
|
}
|
||
|
|
@Override public void onEditRequested(TaskItem taskToEdit) {
|
||
|
|
AddTaskBottomSheet editDialog = new AddTaskBottomSheet();
|
||
|
|
editDialog.setTaskToEdit(taskToEdit);
|
||
|
|
editDialog.setOnTaskAddedListener(new AddTaskBottomSheet.OnTaskAddedListener() {
|
||
|
|
@Override public void onTaskAdded(TaskItem task) {}
|
||
|
|
@Override public void onTaskUpdated(TaskItem task) {
|
||
|
|
saveAndSync();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
editDialog.show(getChildFragmentManager(), "EditTask");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
sheet.show(getChildFragmentManager(), "TaskDetails");
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void onStatusChanged(TaskItem task, boolean isDone) {
|
||
|
|
task.setParticipantStatus(myEmail, isDone);
|
||
|
|
// Her trenger vi ikke logikk for "hasOthers" osv, fordi saveAndSync()
|
||
|
|
// kaller filterAndDisplay() som nå regner ut status dynamisk.
|
||
|
|
saveAndSync();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void saveAndSync() {
|
||
|
|
CacheManager.saveTasks(getContext(), allTasks);
|
||
|
|
filterAndDisplay(); // Oppdaterer visningen umiddelbart
|
||
|
|
RetrofitClient.getApiService().syncTasks(allTasks).enqueue(new Callback<JsonElement>() {
|
||
|
|
@Override public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {}
|
||
|
|
@Override public void onFailure(Call<JsonElement> call, Throwable t) {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\User.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import com.google.gson.annotations.SerializedName;
|
||
|
|
import java.io.Serializable;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class User implements Serializable {
|
||
|
|
@SerializedName("id")
|
||
|
|
private int id;
|
||
|
|
|
||
|
|
@SerializedName("name")
|
||
|
|
private String name;
|
||
|
|
|
||
|
|
@SerializedName("email")
|
||
|
|
private String email;
|
||
|
|
|
||
|
|
@SerializedName("roles")
|
||
|
|
private List<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\UserFilterHelper.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.Arrays;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class UserFilterHelper {
|
||
|
|
|
||
|
|
private static final List<Integer> EXCLUDED_IDS = Arrays.asList(50, 51); // felles@kbs.no og kbs@kbs.no
|
||
|
|
private static final String REQUIRED_DOMAIN = "@kbs.no";
|
||
|
|
|
||
|
|
public static List<User> getFilteredUsers(List<User> allUsers) {
|
||
|
|
if (allUsers == null) return new ArrayList<>();
|
||
|
|
|
||
|
|
UserManager me = UserManager.getInstance();
|
||
|
|
String myEmail = me.getUserEmail();
|
||
|
|
|
||
|
|
List<User> sanitizedList = new ArrayList<>();
|
||
|
|
for (User u : allUsers) {
|
||
|
|
String email = u.getEmail() != null ? u.getEmail().toLowerCase() : "";
|
||
|
|
if (!email.endsWith(REQUIRED_DOMAIN)) continue;
|
||
|
|
if (EXCLUDED_IDS.contains(u.getId())) continue;
|
||
|
|
if (u.getRoles() == null || u.getRoles().isEmpty()) continue;
|
||
|
|
|
||
|
|
sanitizedList.add(u);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (me.isEditorOrAbove()) {
|
||
|
|
return sanitizedList;
|
||
|
|
}
|
||
|
|
|
||
|
|
List<User> finalResult = new ArrayList<>();
|
||
|
|
List<String> myRoles = getRolesForEmail(sanitizedList, myEmail);
|
||
|
|
|
||
|
|
// NY ROLLE LAGT TIL I LISTEN: amuhmsmiljogruppa
|
||
|
|
List<String> deptRoles = Arrays.asList(
|
||
|
|
"serviceavdelingen",
|
||
|
|
"automasjonsavdelingen",
|
||
|
|
"prosjektavdelingen",
|
||
|
|
"administrasjonen",
|
||
|
|
"kbs_alle",
|
||
|
|
"amuhmsmiljogruppa"
|
||
|
|
);
|
||
|
|
|
||
|
|
for (User u : sanitizedList) {
|
||
|
|
if (u.getEmail().equalsIgnoreCase(myEmail)) {
|
||
|
|
finalResult.add(u);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
boolean sharesDepartment = false;
|
||
|
|
for (String role : u.getRoles()) {
|
||
|
|
String r = role.toLowerCase();
|
||
|
|
if (deptRoles.contains(r) && myRoles.contains(r)) {
|
||
|
|
sharesDepartment = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sharesDepartment) {
|
||
|
|
finalResult.add(u);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return finalResult;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static List<String> getRolesForEmail(List<User> users, String email) {
|
||
|
|
List<String> roles = new ArrayList<>();
|
||
|
|
for (User u : users) {
|
||
|
|
if (u.getEmail().equalsIgnoreCase(email)) {
|
||
|
|
if (u.getRoles() != null) {
|
||
|
|
for (String r : u.getRoles()) roles.add(r.toLowerCase());
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return roles;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java
|
||
|
|
============================================================
|
||
|
|
// FILSTI: app\src\main\java\com\kbs\kbsintranett\UserManager.java
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import androidx.annotation.Nullable;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* UserManager fungerer som en global sesjon for appen.
|
||
|
|
* Den holder på informasjon om innlogget bruker, rettigheter og autentiserings-cookie.
|
||
|
|
*/
|
||
|
|
public class UserManager {
|
||
|
|
private static UserManager instance;
|
||
|
|
|
||
|
|
// Google Data
|
||
|
|
private String userDisplayName;
|
||
|
|
private String userEmail;
|
||
|
|
private String googleIdToken;
|
||
|
|
private String photoUrl;
|
||
|
|
|
||
|
|
// WordPress Data
|
||
|
|
private int userId;
|
||
|
|
private String userRole;
|
||
|
|
private String currentCookie;
|
||
|
|
|
||
|
|
// Extended Info
|
||
|
|
private String firstName;
|
||
|
|
private String lastName;
|
||
|
|
private String stilling;
|
||
|
|
private String mobiltelefon;
|
||
|
|
|
||
|
|
// FCM Token (Push)
|
||
|
|
private String fcmToken;
|
||
|
|
|
||
|
|
// NYTT:
|
||
|
|
private List<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 {
|
||
|
|
|
||
|
|
// --- NYHETER ---
|
||
|
|
@GET("wp-json/wp/v2/posts?per_page=10&_embed")
|
||
|
|
Call<List<WpPost>> getPosts();
|
||
|
|
|
||
|
|
@GET("wp-json/wp/v2/posts?per_page=50&_embed")
|
||
|
|
Call<List<WpPost>> getAllPosts();
|
||
|
|
|
||
|
|
|
||
|
|
// --- AUTENTISERING & ENHET ---
|
||
|
|
@POST("wp-json/kbs/v1/login")
|
||
|
|
Call<LoginResponse> googleLogin(@Body LoginRequest request);
|
||
|
|
|
||
|
|
@POST("wp-json/kbs/v1/device/register")
|
||
|
|
Call<JsonElement> registerDevice(@Body RegisterDeviceRequest request);
|
||
|
|
|
||
|
|
@GET("wp-json/kbs/v1/users")
|
||
|
|
Call<List<User>> getUsersList();
|
||
|
|
|
||
|
|
|
||
|
|
// --- KALENDER ---
|
||
|
|
@GET("wp-json/kbs/v1/calendar/events")
|
||
|
|
Call<List<CalendarEvent>> getCalendarEvents();
|
||
|
|
|
||
|
|
@POST("wp-json/kbs/v1/calendar/create")
|
||
|
|
Call<JsonElement> createCalendarEvent(@Body CreateEventRequest request);
|
||
|
|
|
||
|
|
@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);
|
||
|
|
|
||
|
|
|
||
|
|
// --- SKJEMAER (GRAVITY FORMS) ---
|
||
|
|
@GET("wp-json/kbs/v1/forms")
|
||
|
|
Call<List<GravityForm>> getFormsList();
|
||
|
|
|
||
|
|
@GET("wp-json/kbs/v1/forms/{id}")
|
||
|
|
Call<GravityForm> getForm(@Path("id") int formId);
|
||
|
|
|
||
|
|
@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/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);
|
||
|
|
|
||
|
|
|
||
|
|
// --- HÅNDBOK ---
|
||
|
|
@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);
|
||
|
|
|
||
|
|
|
||
|
|
// --- OPPGAVER (SYNKRONISERING) ---
|
||
|
|
@GET("wp-json/kbs/v1/tasks")
|
||
|
|
Call<List<TaskItem>> getTasks();
|
||
|
|
|
||
|
|
@POST("wp-json/kbs/v1/tasks/sync")
|
||
|
|
Call<JsonElement> syncTasks(@Body List<TaskItem> tasks);
|
||
|
|
}
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\java\com\kbs\kbsintranett\WpPost.java
|
||
|
|
============================================================
|
||
|
|
package com.kbs.kbsintranett;
|
||
|
|
|
||
|
|
import com.google.gson.annotations.SerializedName;
|
||
|
|
import java.io.Serializable;
|
||
|
|
import java.util.Arrays;
|
||
|
|
import java.util.List;
|
||
|
|
|
||
|
|
public class WpPost implements Serializable {
|
||
|
|
@SerializedName("title")
|
||
|
|
public Rendered title;
|
||
|
|
|
||
|
|
@SerializedName("excerpt")
|
||
|
|
public Rendered excerpt;
|
||
|
|
|
||
|
|
@SerializedName("content")
|
||
|
|
public Rendered content;
|
||
|
|
|
||
|
|
@SerializedName("date")
|
||
|
|
public String date;
|
||
|
|
|
||
|
|
@SerializedName("_embedded")
|
||
|
|
public Embedded embedded;
|
||
|
|
|
||
|
|
public static class Rendered implements Serializable {
|
||
|
|
@SerializedName("rendered")
|
||
|
|
public String renderedString;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static class Embedded implements Serializable {
|
||
|
|
@SerializedName("wp:featuredmedia")
|
||
|
|
public List<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.drawerlayout.widget.DrawerLayout
|
||
|
|
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/drawer_layout"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent"
|
||
|
|
android:fitsSystemWindows="true"
|
||
|
|
tools:openDrawer="start">
|
||
|
|
|
||
|
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent">
|
||
|
|
|
||
|
|
<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"
|
||
|
|
app:layout_constraintTop_toTopOf="parent"
|
||
|
|
app:layout_constraintLeft_toLeftOf="parent"
|
||
|
|
app:layout_constraintRight_toRightOf="parent" />
|
||
|
|
|
||
|
|
<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_constraintTop_toBottomOf="@id/toolbar"
|
||
|
|
app:layout_constraintBottom_toBottomOf="parent"
|
||
|
|
app:layout_constraintLeft_toLeftOf="parent"
|
||
|
|
app:layout_constraintRight_toRightOf="parent" />
|
||
|
|
|
||
|
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||
|
|
|
||
|
|
<com.google.android.material.navigation.NavigationView
|
||
|
|
android:id="@+id/nav_view"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="match_parent"
|
||
|
|
android:layout_gravity="start"
|
||
|
|
android:fitsSystemWindows="true"
|
||
|
|
app:headerLayout="@layout/nav_header_main"
|
||
|
|
app:menu="@menu/drawer_menu" />
|
||
|
|
|
||
|
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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_add_task.xml
|
||
|
|
============================================================
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:fillViewport="true">
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:orientation="vertical"
|
||
|
|
android:padding="24dp"
|
||
|
|
android:background="@color/white">
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/txt_sheet_title"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Ny Oppgave"
|
||
|
|
android:textSize="20sp"
|
||
|
|
android:textStyle="bold"
|
||
|
|
android:textColor="@color/black"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
<EditText
|
||
|
|
android:id="@+id/et_task_title"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:hint="Hva skal gjøres?"
|
||
|
|
android:inputType="textCapSentences"
|
||
|
|
android:layout_marginBottom="8dp"/>
|
||
|
|
|
||
|
|
<EditText
|
||
|
|
android:id="@+id/et_task_desc"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:hint="Beskrivelse (valgfritt)"
|
||
|
|
android:inputType="textMultiLine"
|
||
|
|
android:minLines="2"
|
||
|
|
android:gravity="top"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:orientation="horizontal"
|
||
|
|
android:gravity="center_vertical"
|
||
|
|
android:layout_marginBottom="12dp">
|
||
|
|
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_task_date"
|
||
|
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Sett frist" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/txt_date_preview"
|
||
|
|
android:layout_width="0dp"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_weight="1"
|
||
|
|
android:text="Frist: -"
|
||
|
|
android:layout_marginStart="8dp"/>
|
||
|
|
|
||
|
|
<!-- NYTT: Fjern frist knapp -->
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_clear_date"
|
||
|
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="X"
|
||
|
|
android:textColor="#999999"
|
||
|
|
android:visibility="gone"/>
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:orientation="horizontal"
|
||
|
|
android:gravity="center_vertical"
|
||
|
|
android:layout_marginBottom="24dp">
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_task_users"
|
||
|
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Velg deltakere" />
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/txt_users_preview"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Kun meg"
|
||
|
|
android:layout_marginStart="8dp"/>
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_save_task"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Lagre Oppgave"
|
||
|
|
android:backgroundTint="@color/kbs_logo_blue"
|
||
|
|
android:textColor="@color/white"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
</LinearLayout>
|
||
|
|
</androidx.core.widget.NestedScrollView>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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\bottom_sheet_task_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="@color/white">
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/detail_task_title"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Oppgavetittel"
|
||
|
|
android:textSize="22sp"
|
||
|
|
android:textStyle="bold"
|
||
|
|
android:textColor="@color/black"
|
||
|
|
android:layout_marginBottom="8dp"/>
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/detail_task_date"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Frist: -"
|
||
|
|
android:textSize="14sp"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/detail_task_desc"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Beskrivelse kommer her..."
|
||
|
|
android:textSize="16sp"
|
||
|
|
android:textColor="#333"
|
||
|
|
android:layout_marginBottom="24dp"
|
||
|
|
android:visibility="gone"/>
|
||
|
|
|
||
|
|
<View
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="1dp"
|
||
|
|
android:background="#EEEEEE"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Status deltakere:"
|
||
|
|
android:textStyle="bold"
|
||
|
|
android:textSize="14sp"
|
||
|
|
android:layout_marginBottom="8dp"/>
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:id="@+id/container_participants_status"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:orientation="vertical"
|
||
|
|
android:layout_marginBottom="24dp"/>
|
||
|
|
|
||
|
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||
|
|
android:id="@+id/switch_notifications"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Aktiver påminnelser til deltakere"
|
||
|
|
android:checked="true"
|
||
|
|
android:layout_marginBottom="24dp"/>
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:id="@+id/layout_owner_actions"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:orientation="horizontal"
|
||
|
|
android:layout_marginBottom="16dp"
|
||
|
|
android:visibility="gone">
|
||
|
|
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_delete_task"
|
||
|
|
android:layout_width="0dp"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_weight="1"
|
||
|
|
android:text="Slett"
|
||
|
|
android:backgroundTint="@color/kbs_logo_accent_red"
|
||
|
|
android:textColor="@color/white"
|
||
|
|
android:layout_marginEnd="8dp"/>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_edit_task"
|
||
|
|
android:layout_width="0dp"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_weight="1"
|
||
|
|
android:text="Endre"
|
||
|
|
android:backgroundTint="@color/kbs_logo_blue"
|
||
|
|
android:textColor="@color/white"
|
||
|
|
android:layout_marginStart="8dp"/>
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
<!-- NY LUKK KNAPP -->
|
||
|
|
<Button
|
||
|
|
android:id="@+id/btn_close_details"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Lukk / Ferdig"
|
||
|
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray"/>
|
||
|
|
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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"?>
|
||
|
|
<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/white">
|
||
|
|
|
||
|
|
<androidx.recyclerview.widget.RecyclerView
|
||
|
|
android:id="@+id/recycler_full_calendar"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent"
|
||
|
|
android:clipToPadding="false"
|
||
|
|
android:paddingBottom="80dp" />
|
||
|
|
|
||
|
|
<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"/>
|
||
|
|
|
||
|
|
<!-- NY FAB: Flytende knapp nederst til høyre -->
|
||
|
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||
|
|
android:id="@+id/fab_add_calendar_event"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_gravity="bottom|end"
|
||
|
|
android:layout_margin="24dp"
|
||
|
|
android:src="@android:drawable/ic_input_add"
|
||
|
|
app:backgroundTint="@color/kbs_logo_blue"
|
||
|
|
app:tint="@color/white"
|
||
|
|
android:visibility="gone"
|
||
|
|
android:contentDescription="Ny hendelse" />
|
||
|
|
|
||
|
|
</FrameLayout>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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">
|
||
|
|
|
||
|
|
<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"
|
||
|
|
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">
|
||
|
|
|
||
|
|
<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="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.\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">
|
||
|
|
|
||
|
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||
|
|
android:id="@+id/swipe_refresh_home"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent">
|
||
|
|
|
||
|
|
<!-- Én enkel RecyclerView som inneholder alt -->
|
||
|
|
<androidx.recyclerview.widget.RecyclerView
|
||
|
|
android:id="@+id/main_recycler_view"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent"
|
||
|
|
android:clipToPadding="false"
|
||
|
|
android:paddingBottom="16dp" />
|
||
|
|
|
||
|
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||
|
|
|
||
|
|
<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">
|
||
|
|
|
||
|
|
<!-- Redundant header er fjernet. Kun kategorilisten og nyhetslisten er igjen. -->
|
||
|
|
|
||
|
|
<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\fragment_tasks.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">
|
||
|
|
|
||
|
|
<com.google.android.material.tabs.TabLayout
|
||
|
|
android:id="@+id/task_tabs"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:background="@color/white"
|
||
|
|
app:tabSelectedTextColor="@color/kbs_logo_blue"
|
||
|
|
app:tabIndicatorColor="@color/kbs_logo_blue">
|
||
|
|
</com.google.android.material.tabs.TabLayout>
|
||
|
|
|
||
|
|
<!-- NY SJEKKBOKS: Vises kun under "Alle" -->
|
||
|
|
<CheckBox
|
||
|
|
android:id="@+id/cb_show_completed"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Vis også fullførte oppgaver"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray"
|
||
|
|
android:textSize="12sp"
|
||
|
|
android:layout_gravity="end"
|
||
|
|
android:layout_marginEnd="16dp"
|
||
|
|
android:layout_marginTop="4dp"
|
||
|
|
android:visibility="gone"/>
|
||
|
|
|
||
|
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||
|
|
android:id="@+id/swipe_refresh_tasks"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="0dp"
|
||
|
|
android:layout_weight="1">
|
||
|
|
|
||
|
|
<androidx.recyclerview.widget.RecyclerView
|
||
|
|
android:id="@+id/recycler_tasks"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="match_parent"
|
||
|
|
android:padding="8dp"
|
||
|
|
android:clipToPadding="false" />
|
||
|
|
|
||
|
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||
|
|
|
||
|
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||
|
|
android:id="@+id/fab_add_task"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_gravity="end"
|
||
|
|
android:layout_margin="24dp"
|
||
|
|
android:src="@android:drawable/ic_input_add"
|
||
|
|
app:backgroundTint="@color/kbs_logo_blue"
|
||
|
|
app:tint="@color/white"
|
||
|
|
android:contentDescription="Ny oppgave" />
|
||
|
|
|
||
|
|
</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_calendar_year_header.xml
|
||
|
|
============================================================
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||
|
|
android:id="@+id/txt_calendar_year"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:background="#4F5B66"
|
||
|
|
android:paddingHorizontal="16dp"
|
||
|
|
android:paddingVertical="8dp"
|
||
|
|
android:text="2026"
|
||
|
|
android:textColor="@color/white"
|
||
|
|
android:textSize="18sp"
|
||
|
|
android:textStyle="bold" />
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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_home_create_btn.xml
|
||
|
|
============================================================
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||
|
|
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="16dp"
|
||
|
|
android:layout_marginBottom="16dp"/>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\res\layout\item_home_empty_tasks.xml
|
||
|
|
============================================================
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Ingen oppgaver tildelt"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray"
|
||
|
|
android:textSize="14sp"
|
||
|
|
android:textStyle="italic"
|
||
|
|
android:paddingHorizontal="16dp"
|
||
|
|
android:paddingVertical="8dp" />
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\res\layout\item_home_header.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"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:padding="16dp">
|
||
|
|
|
||
|
|
<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"
|
||
|
|
app:tint="@color/kbs_logo_blue"/>
|
||
|
|
</RelativeLayout>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\res\layout\item_home_section_title.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="wrap_content"
|
||
|
|
android:orientation="horizontal"
|
||
|
|
android:gravity="center_vertical"
|
||
|
|
android:paddingHorizontal="16dp"
|
||
|
|
android:paddingVertical="8dp">
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/txt_section_title"
|
||
|
|
android:layout_width="0dp"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_weight="1"
|
||
|
|
android:text="Seksjon"
|
||
|
|
android:textSize="18sp"
|
||
|
|
android:textStyle="bold"
|
||
|
|
android:textColor="@color/black"/>
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/btn_view_all"
|
||
|
|
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>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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\layout\item_task.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_marginHorizontal="8dp"
|
||
|
|
android:layout_marginVertical="4dp"
|
||
|
|
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">
|
||
|
|
|
||
|
|
<CheckBox
|
||
|
|
android:id="@+id/task_checkbox"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_marginEnd="8dp" />
|
||
|
|
|
||
|
|
<LinearLayout
|
||
|
|
android:layout_width="0dp"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:layout_weight="1"
|
||
|
|
android:orientation="vertical">
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/task_title"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Oppgavetittel"
|
||
|
|
android:textSize="16sp"
|
||
|
|
android:textStyle="bold"
|
||
|
|
android:textColor="@color/black" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/task_date"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Frist: -"
|
||
|
|
android:textSize="12sp"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/task_creator"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Tildelt av: -"
|
||
|
|
android:textSize="11sp"
|
||
|
|
android:textColor="@color/kbs_logo_blue"
|
||
|
|
android:visibility="gone" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/task_progress"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="Fremdrift: 0/0"
|
||
|
|
android:textSize="11sp"
|
||
|
|
android:textStyle="italic"
|
||
|
|
android:textColor="@color/kbs_muted_blue_gray"
|
||
|
|
android:layout_marginTop="2dp"/>
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
</LinearLayout>
|
||
|
|
</androidx.cardview.widget.CardView>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
FILSTI: app\src\main\res\layout\nav_header_main.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="176dp"
|
||
|
|
android:background="@color/kbs_logo_blue"
|
||
|
|
android:gravity="bottom"
|
||
|
|
android:orientation="vertical"
|
||
|
|
android:padding="16dp"
|
||
|
|
android:theme="@style/ThemeOverlay.AppCompat.Dark">
|
||
|
|
|
||
|
|
<ImageView
|
||
|
|
android:id="@+id/nav_header_image"
|
||
|
|
android:layout_width="64dp"
|
||
|
|
android:layout_height="64dp"
|
||
|
|
android:paddingTop="8dp"
|
||
|
|
android:src="@mipmap/ic_launcher_round" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/nav_header_name"
|
||
|
|
android:layout_width="match_parent"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:paddingTop="8dp"
|
||
|
|
android:text="KBS Intranett"
|
||
|
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||
|
|
android:textStyle="bold" />
|
||
|
|
|
||
|
|
<TextView
|
||
|
|
android:id="@+id/nav_header_email"
|
||
|
|
android:layout_width="wrap_content"
|
||
|
|
android:layout_height="wrap_content"
|
||
|
|
android:text="intranett@kbs.no" />
|
||
|
|
|
||
|
|
</LinearLayout>
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
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\menu\drawer_menu.xml
|
||
|
|
============================================================
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||
|
|
<group android:checkableBehavior="single">
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_home"
|
||
|
|
android:icon="@android:drawable/ic_menu_today"
|
||
|
|
android:title="Hjem" />
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_calendar_full"
|
||
|
|
android:icon="@android:drawable/ic_menu_my_calendar"
|
||
|
|
android:title="Kalender" />
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_tasks"
|
||
|
|
android:icon="@android:drawable/ic_menu_agenda"
|
||
|
|
android:title="Oppgaver" />
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_forms"
|
||
|
|
android:icon="@android:drawable/ic_menu_edit"
|
||
|
|
android:title="Skjemaer" />
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_news_full"
|
||
|
|
android:icon="@android:drawable/ic_menu_gallery"
|
||
|
|
android:title="Nyheter" />
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_handbook"
|
||
|
|
android:icon="@android:drawable/ic_menu_info_details"
|
||
|
|
android:title="Håndbok" />
|
||
|
|
</group>
|
||
|
|
|
||
|
|
<item android:title="Bruker">
|
||
|
|
<menu>
|
||
|
|
<item
|
||
|
|
android:id="@+id/navigation_profile"
|
||
|
|
android:icon="@android:drawable/ic_menu_manage"
|
||
|
|
android:title="Min Profil" />
|
||
|
|
</menu>
|
||
|
|
</item>
|
||
|
|
</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">
|
||
|
|
|
||
|
|
<!-- LOGIN -->
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<!-- HJEM -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_home"
|
||
|
|
android:name="com.kbs.kbsintranett.HomeFragment"
|
||
|
|
android:label="KBS Intranett"
|
||
|
|
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" />
|
||
|
|
<action
|
||
|
|
android:id="@+id/action_home_to_create_event"
|
||
|
|
app:destination="@id/navigation_create_event" />
|
||
|
|
<action
|
||
|
|
android:id="@+id/action_home_to_tasks"
|
||
|
|
app:destination="@id/navigation_tasks" />
|
||
|
|
</fragment>
|
||
|
|
|
||
|
|
<!-- OPPGAVER (Navn endret for Toolbar) -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_tasks"
|
||
|
|
android:name="com.kbs.kbsintranett.TasksFragment"
|
||
|
|
android:label="KBS Oppgaver"
|
||
|
|
tools:layout="@layout/fragment_tasks" />
|
||
|
|
|
||
|
|
<!-- KALENDER (Navn endret for Toolbar) -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_calendar_full"
|
||
|
|
android:name="com.kbs.kbsintranett.CalendarFullFragment"
|
||
|
|
android:label="KBS Kalender"
|
||
|
|
tools:layout="@layout/fragment_calendar_full" />
|
||
|
|
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_create_event"
|
||
|
|
android:name="com.kbs.kbsintranett.CreateEventFragment"
|
||
|
|
android:label="Ny Hendelse"
|
||
|
|
tools:layout="@layout/fragment_create_event" />
|
||
|
|
|
||
|
|
<!-- NYHETER -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_news_full"
|
||
|
|
android:name="com.kbs.kbsintranett.NewsFullFragment"
|
||
|
|
android:label="Siste nytt"
|
||
|
|
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" />
|
||
|
|
|
||
|
|
<!-- SKJEMAER -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_forms"
|
||
|
|
android:name="com.kbs.kbsintranett.FormsListFragment"
|
||
|
|
android:label="KBS 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"
|
||
|
|
android:defaultValue="0"
|
||
|
|
app:argType="integer" />
|
||
|
|
</fragment>
|
||
|
|
|
||
|
|
<!-- HÅNDBOK -->
|
||
|
|
<fragment
|
||
|
|
android:id="@+id/navigation_handbook"
|
||
|
|
android:name="com.kbs.kbsintranett.HandbookFragment"
|
||
|
|
android:label="KBS 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="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>
|
||
|
|
|
||
|
|
<!-- PROFIL -->
|
||
|
|
<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);
|
||
|
|
}
|
||
|
|
}
|