diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java index fb6af0c..46133d6 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java @@ -15,6 +15,38 @@ import java.util.TimeZone; public class CalendarManager { + // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no + private static List getKbsCalendarIds(Context context) { + List ids = new ArrayList<>(); + + String[] projection = new String[] { + CalendarContract.Calendars._ID, + CalendarContract.Calendars.ACCOUNT_NAME + }; + + // Vi ser etter kontoer som slutter på @kbs.no + // (SQL: account_name LIKE '%@kbs.no') + 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)); // Legg til kalender-ID + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return ids; + } + public static List getDeviceEvents(Context context) { List deviceEvents = new ArrayList<>(); @@ -23,10 +55,18 @@ public class CalendarManager { return deviceEvents; } - // ENDRET: Hent events fra 1 år tilbake og 1 år frem + // 1. Finn først ID-ene til KBS-kalenderne + List kbsCalendarIds = getKbsCalendarIds(context); + + // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste + if (kbsCalendarIds.isEmpty()) { + return deviceEvents; + } + + // Hent events fra 1 år tilbake og 1 år frem long now = System.currentTimeMillis(); - long startMillis = now - (365L * 24 * 60 * 60 * 1000); // 1 år tilbake - long endMillis = now + (365L * 24 * 60 * 60 * 1000); // 1 år frem + long startMillis = now - (365L * 24 * 60 * 60 * 1000); + long endMillis = now + (365L * 24 * 60 * 60 * 1000); String[] projection = new String[]{ CalendarContract.Events.TITLE, @@ -34,21 +74,39 @@ public class CalendarManager { CalendarContract.Events.DTEND, CalendarContract.Events.DESCRIPTION, CalendarContract.Events.EVENT_LOCATION, - CalendarContract.Events.ALL_DAY // Nyttig for å vite om det er heldags + CalendarContract.Events.ALL_DAY }; - String selection = CalendarContract.Events.DTSTART + " >= ? AND " + CalendarContract.Events.DTSTART + " <= ?"; - String[] selectionArgs = new String[]{String.valueOf(startMillis), String.valueOf(endMillis)}; + // 2. Bygg opp spørringen for å filtrere på disse ID-ene + // Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?" + StringBuilder selection = new StringBuilder("("); + List selectionArgsList = new ArrayList<>(); + + for (int i = 0; i < kbsCalendarIds.size(); i++) { + selection.append(CalendarContract.Events.CALENDAR_ID).append(" = ?"); + selectionArgsList.add(kbsCalendarIds.get(i)); + if (i < kbsCalendarIds.size() - 1) { + selection.append(" OR "); + } + } + selection.append(") AND "); + + selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); + selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); + + selectionArgsList.add(String.valueOf(startMillis)); + selectionArgsList.add(String.valueOf(endMillis)); + + String[] selectionArgs = selectionArgsList.toArray(new String[0]); try (Cursor cursor = context.getContentResolver().query( CalendarContract.Events.CONTENT_URI, projection, - selection, + selection.toString(), selectionArgs, CalendarContract.Events.DTSTART + " ASC" )) { if (cursor != null) { - // Vi bruker ISO format internt for sortering SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); while (cursor.moveToNext()) { @@ -63,12 +121,10 @@ public class CalendarManager { String rawEnd; if (allDay == 1) { - // For heldags lagrer vi bare datoen: yyyy-MM-dd SimpleDateFormat shortFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); rawStart = shortFormat.format(new Date(dtStart)); rawEnd = shortFormat.format(new Date(dtEnd)); } else { - // For vanlige events bruker vi full tid rawStart = isoFormat.format(new Date(dtStart)); rawEnd = isoFormat.format(new Date(dtEnd)); } @@ -84,7 +140,7 @@ public class CalendarManager { return deviceEvents; } - // --- ROBUST DATO-PARSING (Løser "null" problemet) --- + // --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) --- public static void formatEventForUI(CalendarEvent event) { if (event.getRawDate() == null) return; @@ -102,32 +158,26 @@ public class CalendarManager { String raw = event.getRawDate(); - // SJEKK 1: Er det heldagsdato? (Lengde 10, f.eks "2025-12-31") 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()); } } - // SJEKK 2: Er det ISO format? (Har 'T') else if (raw.contains("T")) { SimpleDateFormat isoFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Viktig for Google events + isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); date = isoFmt.parse(raw); - if (event.getRawEndDate() != null && event.getRawEndDate().contains("T")) { endDate = isoFmt.parse(event.getRawEndDate()); } } - // SJEKK 3: Er det SQL format? (Mellomrom) 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()); } @@ -142,9 +192,6 @@ public class CalendarManager { } else { String timeStr = outputTime.format(date); if (endDate != null) { - // Hvis sluttdato er samme dag, vis bare klokkeslett - // (Enkelt sjekk: hvis datoene er like) - // Her forenkler vi og viser alltid sluttid hvis den finnes timeStr += " - " + outputTime.format(endDate); } event.setTime("Kl. " + timeStr); diff --git a/app/src/main/java/com/kbs/kbsintranett/CategoryAdapter.java b/app/src/main/java/com/kbs/kbsintranett/CategoryAdapter.java new file mode 100644 index 0000000..24aaa57 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/CategoryAdapter.java @@ -0,0 +1,69 @@ +package com.kbs.kbsintranett; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + +public class CategoryAdapter extends RecyclerView.Adapter { + + private List categories; + private String selectedCategory = "Alle"; // Standardvalg + private OnCategoryClickListener listener; + + public interface OnCategoryClickListener { + void onCategoryClick(String category); + } + + public CategoryAdapter(List categories, OnCategoryClickListener listener) { + this.categories = categories; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + String category = categories.get(position); + holder.name.setText(category); + + if (category.equals(selectedCategory)) { + // Valgt stil (Blå bakgrunn, hvit tekst) + holder.name.setBackgroundResource(R.drawable.bg_category_selected); + holder.name.setTextColor(Color.WHITE); + } else { + // Ikke valgt stil (Hvit bakgrunn, mørk tekst) + holder.name.setBackgroundResource(R.drawable.bg_category_unselected); + holder.name.setTextColor(Color.parseColor("#333333")); + } + + holder.itemView.setOnClickListener(v -> { + selectedCategory = category; + notifyDataSetChanged(); // Oppdater alle for å flytte markering + listener.onCategoryClick(category); + }); + } + + @Override + public int getItemCount() { + return categories.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView name; + public ViewHolder(View view) { + super(view); + name = view.findViewById(R.id.category_name); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java index 741f003..93562dc 100644 --- a/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/FormsListFragment.java @@ -2,60 +2,177 @@ 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 java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +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 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); - LinearLayout formsContainer = view.findViewById(R.id.forms_container); + formsContainer = view.findViewById(R.id.forms_container); - // Legger til knappene for de ulike skjemaene - addFormButton(formsContainer, "1. Ansatteopplysninger", 1); - addFormButton(formsContainer, "4. RUH (Rapport om uønsket hendelse)", 4); - addFormButton(formsContainer, "9. Sikkerhetskurs / Kompetansebevis", 9); - addFormButton(formsContainer, "10. HMS-bekreftelse", 10); - addFormButton(formsContainer, "11. Egenmelding", 11); - addFormButton(formsContainer, "12. Sjekkliste for firmabil", 12); - addFormButton(formsContainer, "14. SJA (Sikker Jobbanalyse)", 14); - addFormButton(formsContainer, "15. Fraværsvarsel", 15); - addFormButton(formsContainer, "16. Refusjon utlegg", 16); - addFormButton(formsContainer, "21. Forberedelse til medarbeidersamtale", 21); - addFormButton(formsContainer, "22. Medarbeiderundersøkelse", 22); + 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); + + fetchFormsList(); return view; } + private void fetchFormsList() { + RetrofitClient.getApiService().getFormsListMap().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + + if (response.isSuccessful() && response.body() != null) { + List activeForms = new ArrayList<>(); + + for (GravityForm form : response.body().values()) { + // 1. Sjekk om skjemaet er aktivt (Bruker nå metoden i GravityForm) + if (form.getIsActive()) { + activeForms.add(form); + } + } + + // 2. Sorter basert på tallet i tittelen + Collections.sort(activeForms, new Comparator() { + @Override + public int compare(GravityForm f1, GravityForm f2) { + int num1 = extractNumber(f1.title); + int num2 = extractNumber(f2.title); + return Integer.compare(num1, num2); + } + }); + + populateList(activeForms); + + } else { + String msg = "Kunne ikke hente skjemaer. Kode: " + response.code(); + if (response.code() == 401 || response.code() == 403) { + msg += "\n(Mangler tilgang. Er du admin?)"; + } + showError(msg); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + showError("Nettverksfeil: " + t.getMessage()); + } + }); + } + + private void populateList(List forms) { + formsContainer.removeAllViews(); + + if (forms.isEmpty()) { + showError("Ingen aktive skjemaer funnet."); + return; + } + + for (GravityForm form : forms) { + // FIX: form.id er allerede en int i GravityForm.java, så vi trenger ikke parse + 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")); // KBS Blå + 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); // Litt avstand mellom knappene + params.setMargins(0, 0, 0, 20); btn.setLayoutParams(params); btn.setOnClickListener(v -> { Bundle bundle = new Bundle(); bundle.putInt("formId", formId); - // HER VAR FEILEN: Endret R.id.nav_forms til riktig action ID 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; + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityField.java b/app/src/main/java/com/kbs/kbsintranett/GravityField.java index 59e91ee..8853b4d 100644 --- a/app/src/main/java/com/kbs/kbsintranett/GravityField.java +++ b/app/src/main/java/com/kbs/kbsintranett/GravityField.java @@ -42,13 +42,15 @@ public class GravityField { @SerializedName("content") public String content; + // --- BRUKER ADAPTEREN HER --- + @JsonAdapter(InputsAdapter.class) @SerializedName("inputs") public List inputs; + // --------------------------- @SerializedName("isHidden") public boolean isHidden; - // NYTT: For å sjekke om feltet er Read Only (f.eks dato i refusjon) @SerializedName("gwreadonly_enable") public boolean readOnly; @@ -63,32 +65,19 @@ public class GravityField { public String gpnfForm; public static class Choice { - @SerializedName("text") - public String text; - - @SerializedName("value") - public String value; + @SerializedName("text") public String text; + @SerializedName("value") public String value; } public static class ConditionalLogic { - @SerializedName("actionType") - public String actionType; - - @SerializedName("logicType") - public String logicType; - - @SerializedName("rules") - public List rules; + @SerializedName("actionType") public String actionType; + @SerializedName("logicType") public String logicType; + @SerializedName("rules") public List rules; } public static class Rule { - @SerializedName("fieldId") - public String fieldId; - - @SerializedName("operator") - public String operator; - - @SerializedName("value") - public String value; + @SerializedName("fieldId") public String fieldId; + @SerializedName("operator") public String operator; + @SerializedName("value") public String value; } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/GravityForm.java b/app/src/main/java/com/kbs/kbsintranett/GravityForm.java index da613d0..27ecad9 100644 --- a/app/src/main/java/com/kbs/kbsintranett/GravityForm.java +++ b/app/src/main/java/com/kbs/kbsintranett/GravityForm.java @@ -13,9 +13,17 @@ public class GravityForm { @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 String isActive; // "1" = Aktiv, "0" = Inaktiv + public Object isActive; @SerializedName("fields") public List fields; + + // Hjelpemetode for å sjekke om skjemaet er aktivt + public boolean getIsActive() { + if (isActive == null) return false; + String s = isActive.toString(); + return "1".equals(s) || "true".equalsIgnoreCase(s); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index b0f09e9..56e729d 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -18,8 +18,12 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import retrofit2.Call; import retrofit2.Callback; @@ -33,16 +37,19 @@ public class HomeFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Håndter svar på kalendertillatelse requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), isGranted -> { - // Last kalender på nytt (nå med eller uten personlig kalender) - fetchCalendarEvents(calendarRecycler); + // Prøv å laste kalender på nytt (nå potensielt med personlig kalender) + if (calendarRecycler != null) { + fetchCalendarEvents(calendarRecycler); + } } ); - // Start bakgrunnsjobb for varsling (Hvert 15. minutt) + // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) startNotificationWorker(); } @@ -56,18 +63,19 @@ public class HomeFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Profil-knapp + // 0. Profil-knapp View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); } - // Kalender oppsett + // 1. Kalender oppsett calendarRecycler = view.findViewById(R.id.recycler_calendar); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + // Sett tom adapter midlertidig calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); - // "Se alle" knapp + // "Se alle" knapp for kalender TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); if (viewAllCalendar != null) { viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull)); @@ -88,18 +96,29 @@ public class HomeFragment extends Fragment { } } - // Nyheter + // 2. Nyheter oppsett RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); newsRecycler.setNestedScrollingEnabled(false); - newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>())); + // Sett tom adapter midlertidig + newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); + + // "Se alle" knapp for nyheter + TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); + if (viewAllNews != null) { + viewAllNews.setOnClickListener(v -> { + Navigation.findNavController(view).navigate(R.id.action_home_to_newsFull); + }); + } + fetchNewsFromWordpress(newsRecycler); } private void fetchCalendarEvents(RecyclerView recyclerView) { - // 1. Hent personlige hendelser først + // 1. Hent personlige hendelser først (fra CalendarManager) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); + // 2. Hent API-hendelser fra WordPress WordPressApiService apiService = RetrofitClient.getApiService(); apiService.getCalendarEvents().enqueue(new Callback>() { @Override @@ -114,13 +133,24 @@ public class HomeFragment extends Fragment { } } - // Flett lister + // 3. Flett listene (API + Personlig) og sorter List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); - // Vis kun topp 5 + // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) + // (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende") + List upcomingEvents = new ArrayList<>(); + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + + for (CalendarEvent e : merged) { + if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { + upcomingEvents.add(e); + } + } + + // 5. Vis kun de 5 første av de kommende List top5 = new ArrayList<>(); - for(int i=0; i { @@ -135,6 +165,7 @@ public class HomeFragment extends Fragment { // Hvis API feiler, vis bare personlige events hvis vi har noen if (!deviceEvents.isEmpty()) { List top5 = new ArrayList<>(); + // Filtrer og plukk topp 5 fra lokale også for(int i=0; i { @@ -142,7 +173,6 @@ public class HomeFragment extends Fragment { sheet.show(getParentFragmentManager(), "CalendarDetails"); })); } else { - // Vis feil hvis alt er tomt List errorList = new ArrayList<>(); errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS")); recyclerView.setAdapter(new CalendarAdapter(errorList, null)); @@ -151,48 +181,55 @@ public class HomeFragment extends Fragment { }); } + private void fetchNewsFromWordpress(RecyclerView recyclerView) { + WordPressApiService apiService = RetrofitClient.getApiService(); + // Bruker getPosts som henter 5-10 innlegg med ?_embed + apiService.getPosts().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (getContext() == null) return; + + if (response.isSuccessful() && response.body() != null) { + List wpPosts = response.body(); + + // Datoformatering + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + for (WpPost post : wpPosts) { + try { + Date date = rawFormat.parse(post.date); + post.date = targetFormat.format(date); // Setter pen dato + } catch (Exception e) {} + } + + // Sett adapter med Click Listener + NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { + // Naviger til detaljvisning og send med post-objektet + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); // WpPost er nå Serializable + Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); + }); + recyclerView.setAdapter(adapter); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (getContext() == null) return; + // Ved feil, sett tom liste (eller vis feilmelding) + recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); + } + }); + } + private void startNotificationWorker() { - // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter + // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter i HMS-kalenderen PeriodicWorkRequest notifRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) .build(); WorkManager.getInstance(requireContext()).enqueue(notifRequest); } - - // ... (fetchNewsFromWordpress beholdes som før) ... - private void fetchNewsFromWordpress(RecyclerView recyclerView) { - // [Lim inn koden for nyheter fra forrige svar her, den var OK] - WordPressApiService apiService = RetrofitClient.getApiService(); - apiService.getPosts().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (getContext() == null) return; - if (response.isSuccessful() && response.body() != null) { - List wpPosts = response.body(); - List newsList = new ArrayList<>(); - java.text.SimpleDateFormat rawFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.getDefault()); - rawFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Oslo")); - java.text.SimpleDateFormat targetFormat = new java.text.SimpleDateFormat("dd. MMM yyyy", java.util.Locale.getDefault()); - targetFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Oslo")); - - for (WpPost post : wpPosts) { - String formattedDate = post.date; - try { - java.util.Date date = rawFormat.parse(post.date); - formattedDate = targetFormat.format(date); - } catch (java.text.ParseException e) {} - newsList.add(new NewsItem(post.getTitleStr(), post.getExcerptStr(), "Publisert: " + formattedDate)); - } - recyclerView.setAdapter(new NewsAdapter(newsList)); - } - } - @Override - public void onFailure(Call> call, Throwable t) { - if (getContext() == null) return; - List errorList = new ArrayList<>(); - errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System")); - recyclerView.setAdapter(new NewsAdapter(errorList)); - } - }); - } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/InputsAdapter.java b/app/src/main/java/com/kbs/kbsintranett/InputsAdapter.java new file mode 100644 index 0000000..436963b --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/InputsAdapter.java @@ -0,0 +1,30 @@ +package com.kbs.kbsintranett; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class InputsAdapter implements JsonDeserializer> { + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonNull()) { + return new ArrayList<>(); + } + // Fikser krasjen: Hvis Gravity Forms sender "" i stedet for [], returner tom liste + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + return new ArrayList<>(); + } + if (json.isJsonArray()) { + List list = new ArrayList<>(); + for (JsonElement e : json.getAsJsonArray()) { + list.add(context.deserialize(e, GravityField.class)); + } + return list; + } + return new ArrayList<>(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java b/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java index 767ad75..c823444 100644 --- a/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java +++ b/app/src/main/java/com/kbs/kbsintranett/NewsAdapter.java @@ -3,17 +3,34 @@ package com.kbs.kbsintranett; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import java.util.List; public class NewsAdapter extends RecyclerView.Adapter { - private List newsList; + private List posts; + private OnItemClickListener listener; // NYTT - public NewsAdapter(List newsList) { - this.newsList = newsList; + // Interface for klikk + public interface OnItemClickListener { + void onItemClick(WpPost post); + } + + // Oppdatert konstruktør + public NewsAdapter(List posts, OnItemClickListener listener) { + this.posts = posts; + this.listener = listener; + } + + // Overload for bakoverkompatibilitet (hvis du ikke sender listener) + public NewsAdapter(List posts) { + this.posts = posts; + this.listener = null; } @NonNull @@ -25,24 +42,62 @@ public class NewsAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - NewsItem item = newsList.get(position); - holder.title.setText(item.getTitle()); - holder.excerpt.setText(item.getExcerpt()); - holder.author.setText("Av: " + item.getAuthor()); + 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 newsList.size(); + return posts.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { - TextView title, excerpt, author; + 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); - author = view.findViewById(R.id.news_author); + date = view.findViewById(R.id.news_date); + category = view.findViewById(R.id.news_category); + image = view.findViewById(R.id.news_image); } } + + // NYTT: Metode for å oppdatere listen etter filtrering + public void updateList(List newPosts) { + this.posts = newPosts; + notifyDataSetChanged(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NewsDetailFragment.java b/app/src/main/java/com/kbs/kbsintranett/NewsDetailFragment.java new file mode 100644 index 0000000..01f8481 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/NewsDetailFragment.java @@ -0,0 +1,67 @@ +package com.kbs.kbsintranett; + +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.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 { + + @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); + + // Hent data fra argumentene (sendt fra HomeFragment/NewsFullFragment) + 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); // NY + TextView content = view.findViewById(R.id.detail_content); + + 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); + + // NYTT: Sett forfatter + author.setText("Av: " + post.getAuthorName()); + + content.setText(Html.fromHtml(post.getContentStr(), Html.FROM_HTML_MODE_COMPACT)); + content.setMovementMethod(LinkMovementMethod.getInstance()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NewsFullFragment.java b/app/src/main/java/com/kbs/kbsintranett/NewsFullFragment.java new file mode 100644 index 0000000..38e4475 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/NewsFullFragment.java @@ -0,0 +1,144 @@ +package com.kbs.kbsintranett; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class NewsFullFragment extends Fragment { + + private RecyclerView recyclerViewNews; + private RecyclerView recyclerViewCategories; + private ProgressBar progressBar; + private NewsAdapter newsAdapter; + private List allPosts = new ArrayList<>(); // Holder på ALLE postene + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_news_full, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + recyclerViewNews = view.findViewById(R.id.recycler_news_full); + recyclerViewCategories = view.findViewById(R.id.recycler_categories); + progressBar = view.findViewById(R.id.loading_news_full); + ImageView backBtn = view.findViewById(R.id.btn_back_news); + + // Setup Nyhetsliste + recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext())); + + // Setup Kategorier (Horisontal) + recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); + setupCategories(); + + backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); + + fetchAllNews(); + } + + private void setupCategories() { + // Listen over kategorier du ønsket + List categories = Arrays.asList( + "Alle", // Standard vis alt + "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", + "Ferieavvikling", "Fest og moro", "Generell drift", + "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" + ); + + CategoryAdapter catAdapter = new CategoryAdapter(categories, selectedCategory -> { + filterNews(selectedCategory); + }); + recyclerViewCategories.setAdapter(catAdapter); + } + + private void fetchAllNews() { + progressBar.setVisibility(View.VISIBLE); + // Hent 50 siste (bør holde for en "Siste nytt" liste, ellers må vi paginere) + RetrofitClient.getApiService().getAllPosts().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + + if (response.isSuccessful() && response.body() != null) { + allPosts = response.body(); + formatDates(allPosts); + + // Vis alle i starten + newsAdapter = new NewsAdapter(new ArrayList<>(allPosts), post -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); + Navigation.findNavController(getView()).navigate(R.id.action_newsFull_to_newsDetail, bundle); + }); + recyclerViewNews.setAdapter(newsAdapter); + } else { + Toast.makeText(getContext(), "Klarte ikke laste nyheter", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void filterNews(String category) { + if (newsAdapter == null) return; + + List filteredList = new ArrayList<>(); + + if (category.equals("Alle")) { + filteredList.addAll(allPosts); + } else { + for (WpPost post : allPosts) { + // Vi sjekker om kategorinavnet matcher + if (post.getCategoryName().equals(category)) { + filteredList.add(post); + } + } + } + + // Oppdater adapteren med den filtrerte listen + newsAdapter.updateList(filteredList); + } + + private void formatDates(List posts) { + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + for (WpPost post : posts) { + try { + Date date = rawFormat.parse(post.date); + post.date = targetFormat.format(date); + } catch (Exception e) {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java index 514b60a..077a2f5 100644 --- a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java +++ b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java @@ -16,8 +16,8 @@ import retrofit2.http.PartMap; import retrofit2.http.Query; public interface WordPressApiService { - // 1. Hent nyheter - @GET("wp-json/wp/v2/posts?per_page=5") + // 1. Hent nyheter - ENDRET: Lagt til &_embed for å få bilde og kategori + @GET("wp-json/wp/v2/posts?per_page=10&_embed") Call> getPosts(); // 2. Hent et spesifikt skjema med ID @@ -57,7 +57,11 @@ public interface WordPressApiService { @Query("paging[page_size]") int pageSize ); - // 9. HENT ÉN ENKELT INNSENDING (Ny! Brukes for å hente detaljer fra underskjema) + // 9. HENT ÉN ENKELT INNSENDING @GET("wp-json/gf/v2/entries/{entry_id}") Call getSingleEntry(@Path("entry_id") String entryId); + + // 10. HENT ALLE NYHETER (F.eks 50 stk) - Brukes av "Se alle" siden + @GET("wp-json/wp/v2/posts?per_page=50&_embed") + Call> getAllPosts(); } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WpPost.java b/app/src/main/java/com/kbs/kbsintranett/WpPost.java index d7504e6..b15ae64 100644 --- a/app/src/main/java/com/kbs/kbsintranett/WpPost.java +++ b/app/src/main/java/com/kbs/kbsintranett/WpPost.java @@ -1,31 +1,106 @@ 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 { - // WordPress sender tittelen som et objekt: "title": { "rendered": "Overskrift" } +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; - // Hjelpeklasse for å hente ut teksten inni "rendered" - public static class Rendered { + @SerializedName("_embedded") + public Embedded embedded; + + public static class Rendered implements Serializable { @SerializedName("rendered") public String renderedString; } - // En hjelpemetode for å få ren tekst ut (fjerner HTML-koder hvis nødvendig) + public static class Embedded implements Serializable { + @SerializedName("wp:featuredmedia") + public List mediaList; + + @SerializedName("wp:term") + public List> termList; + + // NYTT: Forfatter-liste + @SerializedName("author") + public List authorList; + } + + public static class Media implements Serializable { + @SerializedName("source_url") + public String sourceUrl; + } + + public static class Term implements Serializable { + @SerializedName("name") + public String name; + } + + // NYTT: Forfatter-klasse + public static class Author implements Serializable { + @SerializedName("name") + public String name; + } + public String getTitleStr() { return title != null ? title.renderedString : "Uten tittel"; } public String getExcerptStr() { - // En enkel rensing av HTML-tags (f.eks

) - return excerpt != null ? android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString() : ""; + return excerpt != null ? + android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString().trim() : ""; + } + + public String getContentStr() { + return content != null ? content.renderedString : ""; + } + + public String getFeaturedImageUrl() { + if (embedded != null && embedded.mediaList != null && !embedded.mediaList.isEmpty()) { + return embedded.mediaList.get(0).sourceUrl; + } + return null; + } + + // NYTT: Hent forfatternavn + public String getAuthorName() { + if (embedded != null && embedded.authorList != null && !embedded.authorList.isEmpty()) { + return embedded.authorList.get(0).name; + } + return "Ukjent"; // Fallback + } + + public String getCategoryName() { + if (embedded != null && embedded.termList != null && !embedded.termList.isEmpty()) { + List categories = embedded.termList.get(0); + if (categories == null || categories.isEmpty()) return ""; + + List priorityCategories = Arrays.asList( + "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", + "Ferieavvikling", "Fest og moro", "Generell drift", + "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" + ); + + for (Term term : categories) { + if (priorityCategories.contains(term.name)) return term.name; + } + for (Term term : categories) { + if (term.name.contains("Alle ansatte")) return "Til info"; + } + return categories.get(0).name; + } + return ""; } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_category_selected.xml b/app/src/main/res/drawable/bg_category_selected.xml new file mode 100644 index 0000000..db347a2 --- /dev/null +++ b/app/src/main/res/drawable/bg_category_selected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_category_unselected.xml b/app/src/main/res/drawable/bg_category_unselected.xml new file mode 100644 index 0000000..6f6b930 --- /dev/null +++ b/app/src/main/res/drawable/bg_category_unselected.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 38a2696..cf97af9 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -21,6 +21,7 @@ android:textStyle="bold" android:textColor="@color/kbs_muted_blue_gray" android:layout_centerVertical="true"/> + - + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_news_full.xml b/app/src/main/res/layout/fragment_news_full.xml new file mode 100644 index 0000000..90f3071 --- /dev/null +++ b/app/src/main/res/layout/fragment_news_full.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml new file mode 100644 index 0000000..e1d39bf --- /dev/null +++ b/app/src/main/res/layout/item_category.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_news.xml b/app/src/main/res/layout/item_news.xml index efc7ebd..ef2afb4 100644 --- a/app/src/main/res/layout/item_news.xml +++ b/app/src/main/res/layout/item_news.xml @@ -3,42 +3,87 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp" - app:cardCornerRadius="8dp" - app:cardElevation="2dp"> + android:layout_marginBottom="24dp" + android:layout_marginHorizontal="8dp" + app:cardCornerRadius="12dp" + app:cardElevation="4dp" + app:cardBackgroundColor="@android:color/white"> + android:orientation="vertical"> - + + + android:orientation="vertical" + android:padding="16dp"> - + - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 520fbab..c6baa7c 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -22,9 +22,18 @@ android:name="com.kbs.kbsintranett.HomeFragment" android:label="Hjem" tools:layout="@layout/fragment_home"> + + + + + + + + + + + - + tools:layout="@layout/fragment_forms_list"> + + + + + + { // [cite: 31] +public class CalendarAdapter extends RecyclerView.Adapter { private List events; - public CalendarAdapter(List events) { // [cite: 32] + private final OnItemClickListener listener; + + public interface OnItemClickListener { + void onItemClick(CalendarEvent event); + } + + // Oppdatert konstruktør som tar imot en listener + public CalendarAdapter(List events, OnItemClickListener listener) { this.events = events; - } // [cite: 33] + this.listener = listener; + } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar, parent, false); - return new ViewHolder(view); // [cite: 34] + return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { CalendarEvent event = events.get(position); - holder.day.setText(event.getDay()); // [cite: 35] + holder.day.setText(event.getDay()); holder.month.setText(event.getMonth()); - // NYTT: Tidspunktet hentes nå fra getTime() som formateres i HomeFragment. holder.time.setText(event.getTime()); holder.title.setText(event.getTitle()); + + holder.itemView.setOnClickListener(v -> { + if (listener != null) { + listener.onItemClick(event); + } + }); } @Override public int getItemCount() { - return events.size(); // [cite: 36] + return events.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { - TextView day, month, title, time; // NYTT: Lagt til time - public ViewHolder(View view) { // [cite: 37] + TextView day, month, title, time; + + public ViewHolder(View view) { super(view); day = view.findViewById(R.id.cal_day); - month = view.findViewById(R.id.cal_month); // [cite: 38] + month = view.findViewById(R.id.cal_month); title = view.findViewById(R.id.cal_title); - time = view.findViewById(R.id.cal_time); // NYTT + time = view.findViewById(R.id.cal_time); + } + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarDetailsBottomSheet.java +============================================================ +package com.kbs.kbsintranett; + +import android.content.Intent; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment { + + private CalendarEvent event; + + public CalendarDetailsBottomSheet(CalendarEvent event) { + this.event = event; + } + + @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); + Button btnAdd = view.findViewById(R.id.btn_add_to_calendar); + + // Skjul knapp siden appen nå varsler automatisk (iht krav) + btnAdd.setVisibility(View.GONE); + + title.setText(event.getTitle()); + time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")"); + + if (!event.getDescription().isEmpty()) { + // HER ER FIKSEN FOR HTML: + desc.setText(android.text.Html.fromHtml(event.getDescription(), android.text.Html.FROM_HTML_MODE_COMPACT)); + desc.setVisibility(View.VISIBLE); + // Gjør linker klikkbare + desc.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); + } else { + desc.setVisibility(View.GONE); + } + + if (!event.getLocation().isEmpty()) { + loc.setText("Sted: " + event.getLocation()); + loc.setVisibility(View.VISIBLE); + } else { + loc.setVisibility(View.GONE); + } + + return view; + } + + private void addToSystemCalendar() { + try { + SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + Date startDate = apiFormat.parse(event.getRawDate()); + long startMillis = startDate.getTime(); + long endMillis = startMillis + (60 * 60 * 1000); // Default 1 time hvis slutt mangler + + if (event.getRawEndDate() != null && !event.getRawEndDate().isEmpty()) { + Date endDate = apiFormat.parse(event.getRawEndDate()); + endMillis = endDate.getTime(); + } + + Intent intent = new Intent(Intent.ACTION_INSERT) + .setData(CalendarContract.Events.CONTENT_URI) + .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) + .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) + .putExtra(CalendarContract.Events.TITLE, event.getTitle()) + .putExtra(CalendarContract.Events.DESCRIPTION, event.getDescription()) + .putExtra(CalendarContract.Events.EVENT_LOCATION, event.getLocation()) + .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY); + + startActivity(intent); + + } catch (Exception e) { + e.printStackTrace(); } } } @@ -347,16 +466,41 @@ public class CalendarAdapter extends RecyclerView.Adapter Navigation.findNavController(view).navigateUp()); + + fetchAllEvents(); + } + + private void fetchAllEvents() { + progressBar.setVisibility(View.VISIBLE); + + // Hent personlige hendelser (Nå med historikk) + List deviceEvents = CalendarManager.getDeviceEvents(getContext()); + + RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + + List apiEvents = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null) { + for (CalendarEvent e : response.body()) { + CalendarManager.formatEventForUI(e); + apiEvents.add(e); + } + } + + // Flett og vis + List allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents); + + if (allEvents.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + + CalendarAdapter adapter = new CalendarAdapter(allEvents, event -> { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + }); + recyclerView.setAdapter(adapter); + + // --- SCROLL TIL I DAG --- + scrollToToday(allEvents); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + + if (!deviceEvents.isEmpty()) { + CalendarAdapter adapter = new CalendarAdapter(deviceEvents, event -> { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + }); + recyclerView.setAdapter(adapter); + scrollToToday(deviceEvents); + } else { + emptyView.setText("Ingen hendelser funnet."); + emptyView.setVisibility(View.VISIBLE); + } + } + }); + } + + private void scrollToToday(List events) { + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + int scrollIndex = 0; + + // Finn første event som er i dag eller senere + for (int i = 0; i < events.size(); i++) { + String raw = events.get(i).getRawDate(); + if (raw != null && raw.compareTo(today) >= 0) { + scrollIndex = i; + break; + } + } + + // Scroll litt ned slik at "i dag" havner på toppen, men ikke helt (offset 0) + // Bruker scrollToPositionWithOffset for presisjon + if (scrollIndex > 0) { + layoutManager.scrollToPositionWithOffset(scrollIndex, 0); + } + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarManager.java +============================================================ +package com.kbs.kbsintranett; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.provider.CalendarContract; +import androidx.core.content.ContextCompat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +public class CalendarManager { + + // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no + private static List getKbsCalendarIds(Context context) { + List ids = new ArrayList<>(); + + String[] projection = new String[] { + CalendarContract.Calendars._ID, + CalendarContract.Calendars.ACCOUNT_NAME + }; + + // Vi ser etter kontoer som slutter på @kbs.no + // (SQL: account_name LIKE '%@kbs.no') + 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)); // Legg til kalender-ID + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return ids; + } + + public static List getDeviceEvents(Context context) { + List deviceEvents = new ArrayList<>(); + + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + return deviceEvents; + } + + // 1. Finn først ID-ene til KBS-kalenderne + List kbsCalendarIds = getKbsCalendarIds(context); + + // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste + if (kbsCalendarIds.isEmpty()) { + return deviceEvents; + } + + // Hent events fra 1 år tilbake og 1 år frem + long now = System.currentTimeMillis(); + long startMillis = now - (365L * 24 * 60 * 60 * 1000); + long endMillis = now + (365L * 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 + }; + + // 2. Bygg opp spørringen for å filtrere på disse ID-ene + // Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?" + StringBuilder selection = new StringBuilder("("); + List selectionArgsList = new ArrayList<>(); + + for (int i = 0; i < kbsCalendarIds.size(); i++) { + selection.append(CalendarContract.Events.CALENDAR_ID).append(" = ?"); + selectionArgsList.add(kbsCalendarIds.get(i)); + if (i < kbsCalendarIds.size() - 1) { + selection.append(" OR "); + } + } + selection.append(") AND "); + + selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); + selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); + + selectionArgsList.add(String.valueOf(startMillis)); + selectionArgsList.add(String.valueOf(endMillis)); + + String[] selectionArgs = selectionArgsList.toArray(new String[0]); + + try (Cursor cursor = context.getContentResolver().query( + CalendarContract.Events.CONTENT_URI, + projection, + selection.toString(), + selectionArgs, + CalendarContract.Events.DTSTART + " ASC" + )) { + if (cursor != null) { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + + while (cursor.moveToNext()) { + String title = cursor.getString(0); + long dtStart = cursor.getLong(1); + long dtEnd = cursor.getLong(2); + String desc = cursor.getString(3); + String loc = cursor.getString(4); + int allDay = cursor.getInt(5); + + String rawStart; + String rawEnd; + + if (allDay == 1) { + SimpleDateFormat shortFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + rawStart = shortFormat.format(new Date(dtStart)); + rawEnd = shortFormat.format(new Date(dtEnd)); + } else { + rawStart = isoFormat.format(new Date(dtStart)); + rawEnd = isoFormat.format(new Date(dtEnd)); + } + + CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc); + formatEventForUI(event); + deviceEvents.add(event); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return deviceEvents; + } + + // --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) --- + public static void formatEventForUI(CalendarEvent event) { + if (event.getRawDate() == null) return; + + SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); + SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); + SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); + + outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + try { + Date date = null; + Date endDate = null; + boolean isAllDay = false; + + String raw = event.getRawDate(); + + if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { + SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + date = shortFmt.parse(raw); + isAllDay = true; + if (event.getRawEndDate() != null && event.getRawEndDate().length() == 10) { + endDate = shortFmt.parse(event.getRawEndDate()); + } + } + else if (raw.contains("T")) { + SimpleDateFormat isoFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + date = isoFmt.parse(raw); + if (event.getRawEndDate() != null && event.getRawEndDate().contains("T")) { + endDate = isoFmt.parse(event.getRawEndDate()); + } + } + else { + SimpleDateFormat sqlFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + sqlFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + date = sqlFmt.parse(raw); + if (event.getRawEndDate() != null) { + endDate = sqlFmt.parse(event.getRawEndDate()); + } + } + + if (date != null) { + event.setDay(outputDay.format(date)); + event.setMonth(outputMonth.format(date).toUpperCase()); + + if (isAllDay) { + event.setTime("Hele dagen"); + } else { + String timeStr = outputTime.format(date); + if (endDate != null) { + timeStr += " - " + outputTime.format(endDate); + } + event.setTime("Kl. " + timeStr); + } + } + + } catch (Exception e) { + e.printStackTrace(); + event.setDay("??"); + event.setMonth("???"); + event.setTime("Feil dato"); + } + } + + public static List mergeAndSort(List apiEvents, List deviceEvents) { + List all = new ArrayList<>(apiEvents); + all.addAll(deviceEvents); + + 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; + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\CategoryAdapter.java +============================================================ +package com.kbs.kbsintranett; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + +public class CategoryAdapter extends RecyclerView.Adapter { + + private List categories; + private String selectedCategory = "Alle"; // Standardvalg + private OnCategoryClickListener listener; + + public interface OnCategoryClickListener { + void onCategoryClick(String category); + } + + public CategoryAdapter(List categories, OnCategoryClickListener listener) { + this.categories = categories; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + String category = categories.get(position); + holder.name.setText(category); + + if (category.equals(selectedCategory)) { + // Valgt stil (Blå bakgrunn, hvit tekst) + holder.name.setBackgroundResource(R.drawable.bg_category_selected); + holder.name.setTextColor(Color.WHITE); + } else { + // Ikke valgt stil (Hvit bakgrunn, mørk tekst) + holder.name.setBackgroundResource(R.drawable.bg_category_unselected); + holder.name.setTextColor(Color.parseColor("#333333")); + } + + holder.itemView.setOnClickListener(v -> { + selectedCategory = category; + notifyDataSetChanged(); // Oppdater alle for å flytte markering + listener.onCategoryClick(category); + }); + } + + @Override + public int getItemCount() { + return categories.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView name; + public ViewHolder(View view) { + super(view); + name = view.findViewById(R.id.category_name); + } + } } ============================================================ @@ -2381,207 +2965,240 @@ public class HandbookFragment extends Fragment { ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java ============================================================ -// 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.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; +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 java.text.ParseException; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; - +import java.util.concurrent.TimeUnit; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class HomeFragment extends Fragment { + private ActivityResultLauncher requestPermissionLauncher; + private RecyclerView calendarRecycler; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Håndter svar på kalendertillatelse + requestPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + isGranted -> { + // Prøv å laste kalender på nytt (nå potensielt med personlig kalender) + if (calendarRecycler != null) { + fetchCalendarEvents(calendarRecycler); + } + } + ); + + // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) + startNotificationWorker(); + } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - // Laster inn layouten fra XML (fragment_home.xml) return inflater.inflate(R.layout.fragment_home, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // --------------------------------------------------------- - // 0. SETT OPP PROFIL-KNAPP (Ny!) - // --------------------------------------------------------- - // Vi finner ikonet vi la til i XML og sier at det skal gå til Profil-siden + + // 0. Profil-knapp View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { - profileBtn.setOnClickListener(v -> { - Navigation.findNavController(view).navigate(R.id.navigation_profile); + profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile)); + } + + // 1. Kalender oppsett + calendarRecycler = view.findViewById(R.id.recycler_calendar); + calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + // Sett tom adapter midlertidig + calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); + + // "Se alle" knapp for kalender + TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); + if (viewAllCalendar != null) { + viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull)); + } + + // Sjekk tillatelse for personlig kalender + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { + fetchCalendarEvents(calendarRecycler); + } else { + // Spør om lov til å lese kalender + requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); + } + + // Spør også om lov til å sende varsler (Android 13+) + if (android.os.Build.VERSION.SDK_INT >= 33) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + + // 2. Nyheter oppsett + RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); + newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + newsRecycler.setNestedScrollingEnabled(false); + // Sett tom adapter midlertidig + newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); + + // "Se alle" knapp for nyheter + TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); + if (viewAllNews != null) { + viewAllNews.setOnClickListener(v -> { + Navigation.findNavController(view).navigate(R.id.action_home_to_newsFull); }); } - // --------------------------------------------------------- - // 1. SETT OPP KALENDER (Henter fra WordPress) - // --------------------------------------------------------- - RecyclerView calendarRecycler = view.findViewById(R.id.recycler_calendar); - calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); - - // Starter henting av kalenderdata - fetchCalendarEvents(calendarRecycler); - - // --------------------------------------------------------- - // 2. SETT OPP NYHETER (Hentes fra WordPress) - // --------------------------------------------------------- - RecyclerView newsRecycler = view.findViewById(R.id.recycler_news); - newsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); - - // Gjør at scrollen flyter bedre inni NestedScrollView (hvis du bruker det i XML) - newsRecycler.setNestedScrollingEnabled(false); - // Start henting av ekte data fetchNewsFromWordpress(newsRecycler); } - /** - * Henter kalenderhendelser fra WordPress via RetrofitClient - */ private void fetchCalendarEvents(RecyclerView recyclerView) { - // 1. Hent API-tjenesten vår + // 1. Hent personlige hendelser først (fra CalendarManager) + List deviceEvents = CalendarManager.getDeviceEvents(getContext()); + + // 2. Hent API-hendelser fra WordPress WordPressApiService apiService = RetrofitClient.getApiService(); - // 2. Send forespørsel til nettet (Asynkront) apiService.getCalendarEvents().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { - if (getContext() == null || response.body() == null) return; + if (!isAdded()) return; - List rawEvents = response.body(); - List formattedEvents = new ArrayList<>(); - - // Formater for parsing og visning - SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone - - SimpleDateFormat dayFormat = new SimpleDateFormat("dd", Locale.getDefault()); - dayFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone - - // Bruker norsk locale for måned (Jan, Feb, Mar, etc.) - SimpleDateFormat monthFormat = new SimpleDateFormat("MMM", new Locale("no", "NO")); - monthFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone - - SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault()); - timeFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone - - for (CalendarEvent event : rawEvents) { - try { - // Bruker getRawDate() fra CalendarEvent.java (oppdatert) - Date date = apiFormat.parse(event.getRawDate()); - String day = dayFormat.format(date); - String month = monthFormat.format(date).toUpperCase(Locale.getDefault()); - String startTime = timeFormat.format(date); - - // Bruker den gamle konstruktøren for å sette formaterte data i adapteren - formattedEvents.add(new CalendarEvent( - event.getTitle(), - startTime, - day, - month - )); - } catch (ParseException e) { - e.printStackTrace(); - // Håndterer feil i parsing av dato/tid ved å vise rå data - formattedEvents.add(new CalendarEvent(event.getTitle(), "Ukjent", event.getDay(), event.getMonth())); + List apiEvents = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null) { + for (CalendarEvent e : response.body()) { + CalendarManager.formatEventForUI(e); // Formatér datoer + apiEvents.add(e); } } - recyclerView.setAdapter(new CalendarAdapter(formattedEvents)); + + // 3. Flett listene (API + Personlig) og sorter + List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); + + // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) + // (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende") + List upcomingEvents = new ArrayList<>(); + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + + for (CalendarEvent e : merged) { + if (e.getRawDate() != null && e.getRawDate().compareTo(today) >= 0) { + upcomingEvents.add(e); + } + } + + // 5. Vis kun de 5 første av de kommende + List top5 = new ArrayList<>(); + for(int i=0; i { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + })); } @Override public void onFailure(Call> call, Throwable t) { - if (getContext() == null) return; - System.err.println("Kalender Nettverksfeil: " + t.getMessage()); - // Vis feilmelding i RecyclerView - List errorList = new ArrayList<>(); - errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverket ditt.", "00", "FEIL")); - recyclerView.setAdapter(new CalendarAdapter(errorList)); + if (!isAdded()) return; + // Hvis API feiler, vis bare personlige events hvis vi har noen + if (!deviceEvents.isEmpty()) { + List top5 = new ArrayList<>(); + // Filtrer og plukk topp 5 fra lokale også + for(int i=0; i { + CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); + sheet.show(getParentFragmentManager(), "CalendarDetails"); + })); + } else { + List errorList = new ArrayList<>(); + errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS")); + recyclerView.setAdapter(new CalendarAdapter(errorList, null)); + } } }); } - /** - * Henter nyheter fra WordPress via RetrofitClient - */ private void fetchNewsFromWordpress(RecyclerView recyclerView) { - // 1. Hent API-tjenesten vår WordPressApiService apiService = RetrofitClient.getApiService(); - // 2. Send forespørsel til nettet (Asynkront) + // Bruker getPosts som henter 5-10 innlegg med ?_embed apiService.getPosts().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { - // Sjekk om appen fortsatt lever (viktig for å unngå krasj) if (getContext() == null) return; if (response.isSuccessful() && response.body() != null) { - // 3. Suksess! Vi fikk data fra WordPress. List wpPosts = response.body(); - List newsList = new ArrayList<>(); - // Datoformatering for nyhetene + // Datoformatering SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone - + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); - targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); - // Konverter fra "WpPost" (API-format) til "NewsItem" (App-format) for (WpPost post : wpPosts) { - String formattedDate = post.date; - try { - // API-datoen (post.date) er i formatet "yyyy-MM-dd'T'HH:mm:ss" Date date = rawFormat.parse(post.date); - formattedDate = targetFormat.format(date); - } catch (ParseException e) { - System.err.println("Feil ved parsing av nyhetsdato: " + e.getMessage()); - } - - newsList.add(new NewsItem( - post.getTitleStr(), - post.getExcerptStr(), - "Publisert: " + formattedDate - )); + post.date = targetFormat.format(date); // Setter pen dato + } catch (Exception e) {} } - // 4. Send listen til Adapteren slik at den vises på skjermen - NewsAdapter adapter = new NewsAdapter(newsList); + // Sett adapter med Click Listener + NewsAdapter adapter = new NewsAdapter(wpPosts, post -> { + // Naviger til detaljvisning og send med post-objektet + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); // WpPost er nå Serializable + Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle); + }); recyclerView.setAdapter(adapter); - } else { - System.err.println("Feil: Fikk svar, men noe var galt med dataene: " + response.code()); - // Her kunne vi vist en "Ingen nyheter"-tekst - // (Løsningen har allerede lagt inn fallback i onFailure) } } @Override public void onFailure(Call> call, Throwable t) { - // Nettverksfeil (Ingen nett, feil URL, etc) if (getContext() == null) return; - System.err.println("Nettverksfeil: " + t.getMessage()); - // Legg til en "Feilmelding" i listen så brukeren ser det - List errorList = new ArrayList<>(); - errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System")); - recyclerView.setAdapter(new NewsAdapter(errorList)); + // Ved feil, sett tom liste (eller vis feilmelding) + recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null)); } }); } + + private void startNotificationWorker() { + // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter i HMS-kalenderen + PeriodicWorkRequest notifRequest = + new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) + .build(); + WorkManager.getInstance(requireContext()).enqueue(notifRequest); + } } ============================================================ @@ -2878,17 +3495,34 @@ package com.kbs.kbsintranett; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import java.util.List; public class NewsAdapter extends RecyclerView.Adapter { - private List newsList; + private List posts; + private OnItemClickListener listener; // NYTT - public NewsAdapter(List newsList) { - this.newsList = newsList; + // Interface for klikk + public interface OnItemClickListener { + void onItemClick(WpPost post); + } + + // Oppdatert konstruktør + public NewsAdapter(List posts, OnItemClickListener listener) { + this.posts = posts; + this.listener = listener; + } + + // Overload for bakoverkompatibilitet (hvis du ikke sender listener) + public NewsAdapter(List posts) { + this.posts = posts; + this.listener = null; } @NonNull @@ -2900,24 +3534,281 @@ public class NewsAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - NewsItem item = newsList.get(position); - holder.title.setText(item.getTitle()); - holder.excerpt.setText(item.getExcerpt()); - holder.author.setText("Av: " + item.getAuthor()); + 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 newsList.size(); + return posts.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { - TextView title, excerpt, author; + 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); - author = view.findViewById(R.id.news_author); + date = view.findViewById(R.id.news_date); + category = view.findViewById(R.id.news_category); + image = view.findViewById(R.id.news_image); + } + } + + // NYTT: Metode for å oppdatere listen etter filtrering + public void updateList(List newPosts) { + this.posts = newPosts; + notifyDataSetChanged(); + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsDetailFragment.java +============================================================ +package com.kbs.kbsintranett; + +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.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 { + + @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); + + // Hent data fra argumentene (sendt fra HomeFragment/NewsFullFragment) + 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); // NY + TextView content = view.findViewById(R.id.detail_content); + + 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); + + // NYTT: Sett forfatter + author.setText("Av: " + post.getAuthorName()); + + content.setText(Html.fromHtml(post.getContentStr(), Html.FROM_HTML_MODE_COMPACT)); + content.setMovementMethod(LinkMovementMethod.getInstance()); + } +} + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsFullFragment.java +============================================================ +package com.kbs.kbsintranett; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class NewsFullFragment extends Fragment { + + private RecyclerView recyclerViewNews; + private RecyclerView recyclerViewCategories; + private ProgressBar progressBar; + private NewsAdapter newsAdapter; + private List allPosts = new ArrayList<>(); // Holder på ALLE postene + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_news_full, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + recyclerViewNews = view.findViewById(R.id.recycler_news_full); + recyclerViewCategories = view.findViewById(R.id.recycler_categories); + progressBar = view.findViewById(R.id.loading_news_full); + ImageView backBtn = view.findViewById(R.id.btn_back_news); + + // Setup Nyhetsliste + recyclerViewNews.setLayoutManager(new LinearLayoutManager(getContext())); + + // Setup Kategorier (Horisontal) + recyclerViewCategories.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); + setupCategories(); + + backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); + + fetchAllNews(); + } + + private void setupCategories() { + // Listen over kategorier du ønsket + List categories = Arrays.asList( + "Alle", // Standard vis alt + "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", + "Ferieavvikling", "Fest og moro", "Generell drift", + "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" + ); + + CategoryAdapter catAdapter = new CategoryAdapter(categories, selectedCategory -> { + filterNews(selectedCategory); + }); + recyclerViewCategories.setAdapter(catAdapter); + } + + private void fetchAllNews() { + progressBar.setVisibility(View.VISIBLE); + // Hent 50 siste (bør holde for en "Siste nytt" liste, ellers må vi paginere) + RetrofitClient.getApiService().getAllPosts().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + + if (response.isSuccessful() && response.body() != null) { + allPosts = response.body(); + formatDates(allPosts); + + // Vis alle i starten + newsAdapter = new NewsAdapter(new ArrayList<>(allPosts), post -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("post_data", post); + Navigation.findNavController(getView()).navigate(R.id.action_newsFull_to_newsDetail, bundle); + }); + recyclerViewNews.setAdapter(newsAdapter); + } else { + Toast.makeText(getContext(), "Klarte ikke laste nyheter", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (!isAdded()) return; + progressBar.setVisibility(View.GONE); + Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void filterNews(String category) { + if (newsAdapter == null) return; + + List filteredList = new ArrayList<>(); + + if (category.equals("Alle")) { + filteredList.addAll(allPosts); + } else { + for (WpPost post : allPosts) { + // Vi sjekker om kategorinavnet matcher + if (post.getCategoryName().equals(category)) { + filteredList.add(post); + } + } + } + + // Oppdater adapteren med den filtrerte listen + newsAdapter.updateList(filteredList); + } + + private void formatDates(List posts) { + SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault()); + targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); + + for (WpPost post : posts) { + try { + Date date = rawFormat.parse(post.date); + post.date = targetFormat.format(date); + } catch (Exception e) {} } } } @@ -2943,6 +3834,109 @@ public class NewsItem { public String getAuthor() { return author; } } +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationWorker.java +============================================================ +package com.kbs.kbsintranett; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import retrofit2.Response; + +public class NotificationWorker extends Worker { + + private static final String CHANNEL_ID = "kbs_calendar_channel"; + private static final String PREFS_NAME = "KBSNotificationPrefs"; + + public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + // Dette kjører i bakgrunnen + try { + // Hent events synkront (ikke enqueue) + Response> response = RetrofitClient.getApiService().getCalendarEvents().execute(); + + if (response.isSuccessful() && response.body() != null) { + checkAndNotify(response.body()); + return Result.success(); + } else { + return Result.retry(); + } + } catch (IOException e) { + return Result.retry(); + } + } + + private void checkAndNotify(List events) { + SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + long now = System.currentTimeMillis(); + long fifteenMinutes = 15 * 60 * 1000; + + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + + for (CalendarEvent event : events) { + try { + Date eventDate; + if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); + else eventDate = sqlFormat.parse(event.getRawDate()); + + if (eventDate == null) continue; + + long diff = eventDate.getTime() - now; + + // Hvis eventet starter innen de neste 30 min, og ikke allerede varslet + if (diff > 0 && diff < (30 * 60 * 1000)) { + String eventId = event.getTitle() + event.getRawDate(); // Enkel ID + boolean alreadyNotified = prefs.getBoolean(eventId, false); + + if (!alreadyNotified) { + sendNotification(event.getTitle(), "Starter kl " + event.getTime()); + // Lagre at vi har varslet + prefs.edit().putBoolean(eventId, true).apply(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void sendNotification(String title, String content) { + NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); + manager.createNotificationChannel(channel); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her + .setContentTitle(title) + .setContentText(content) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true); + + manager.notify((int) System.currentTimeMillis(), builder.build()); + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\ProfileFragment.java ============================================================ @@ -3048,32 +4042,30 @@ import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitClient { private static final String BASE_URL = "https://intranet.kbs.no/"; - - // VI FJERNER FAKE_COOKIE HERFRA! Den trengs ikke lenger. - private static Retrofit retrofit = null; public static WordPressApiService getApiService() { - // Vi må bygge klienten på nytt hvis vi logger ut/inn, men for enkelhets skyld - // sjekker vi bare null her. if (retrofit == null) { + // NYTT: Logging Interceptor + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers) + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(logging) // Legg til loggingen her .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder(); - // 1. Hent cookie fra UserManager String dynamicCookie = UserManager.getInstance().getCookie(); - - // 2. Hvis vi har en cookie, legg den til i headeren if (dynamicCookie != null && !dynamicCookie.isEmpty()) { builder.header("Cookie", dynamicCookie); } @@ -3085,6 +4077,7 @@ public class RetrofitClient { Gson gson = new GsonBuilder() .registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter()) + .setLenient() // NYTT: Gjør parsing litt mer tilgivende .create(); retrofit = new Retrofit.Builder() @@ -3096,7 +4089,6 @@ public class RetrofitClient { return retrofit.create(WordPressApiService.class); } - // Hjelpemetode for å nullstille Retrofit ved utlogging public static void clearClient() { retrofit = null; } @@ -3248,8 +4240,8 @@ import retrofit2.http.PartMap; import retrofit2.http.Query; public interface WordPressApiService { - // 1. Hent nyheter - @GET("wp-json/wp/v2/posts?per_page=5") + // 1. Hent nyheter - ENDRET: Lagt til &_embed for å få bilde og kategori + @GET("wp-json/wp/v2/posts?per_page=10&_embed") Call> getPosts(); // 2. Hent et spesifikt skjema med ID @@ -3289,9 +4281,13 @@ public interface WordPressApiService { @Query("paging[page_size]") int pageSize ); - // 9. HENT ÉN ENKELT INNSENDING (Ny! Brukes for å hente detaljer fra underskjema) + // 9. HENT ÉN ENKELT INNSENDING @GET("wp-json/gf/v2/entries/{entry_id}") Call getSingleEntry(@Path("entry_id") String entryId); + + // 10. HENT ALLE NYHETER (F.eks 50 stk) - Brukes av "Se alle" siden + @GET("wp-json/wp/v2/posts?per_page=50&_embed") + Call> getAllPosts(); } ============================================================ @@ -3300,35 +4296,129 @@ 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 { - // WordPress sender tittelen som et objekt: "title": { "rendered": "Overskrift" } +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; - // Hjelpeklasse for å hente ut teksten inni "rendered" - public static class Rendered { + @SerializedName("_embedded") + public Embedded embedded; + + public static class Rendered implements Serializable { @SerializedName("rendered") public String renderedString; } - // En hjelpemetode for å få ren tekst ut (fjerner HTML-koder hvis nødvendig) + public static class Embedded implements Serializable { + @SerializedName("wp:featuredmedia") + public List mediaList; + + @SerializedName("wp:term") + public List> termList; + + // NYTT: Forfatter-liste + @SerializedName("author") + public List authorList; + } + + public static class Media implements Serializable { + @SerializedName("source_url") + public String sourceUrl; + } + + public static class Term implements Serializable { + @SerializedName("name") + public String name; + } + + // NYTT: Forfatter-klasse + public static class Author implements Serializable { + @SerializedName("name") + public String name; + } + public String getTitleStr() { return title != null ? title.renderedString : "Uten tittel"; } public String getExcerptStr() { - // En enkel rensing av HTML-tags (f.eks

) - return excerpt != null ? android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString() : ""; + return excerpt != null ? + android.text.Html.fromHtml(excerpt.renderedString, android.text.Html.FROM_HTML_MODE_COMPACT).toString().trim() : ""; + } + + public String getContentStr() { + return content != null ? content.renderedString : ""; + } + + public String getFeaturedImageUrl() { + if (embedded != null && embedded.mediaList != null && !embedded.mediaList.isEmpty()) { + return embedded.mediaList.get(0).sourceUrl; + } + return null; + } + + // NYTT: Hent forfatternavn + public String getAuthorName() { + if (embedded != null && embedded.authorList != null && !embedded.authorList.isEmpty()) { + return embedded.authorList.get(0).name; + } + return "Ukjent"; // Fallback + } + + public String getCategoryName() { + if (embedded != null && embedded.termList != null && !embedded.termList.isEmpty()) { + List categories = embedded.termList.get(0); + if (categories == null || categories.isEmpty()) return ""; + + List priorityCategories = Arrays.asList( + "Avtaler og invitasjoner", "BHT", "Bilhold", "Cordel", + "Ferieavvikling", "Fest og moro", "Generell drift", + "HMS", "IT og sikkerhet", "Miljøfyrtårn", "Møtereferat", "SMX" + ); + + for (Term term : categories) { + if (priorityCategories.contains(term.name)) return term.name; + } + for (Term term : categories) { + if (term.name.contains("Alle ansatte")) return "Til info"; + } + return categories.get(0).name; + } + return ""; } } +============================================================ +FILSTI: app\src\main\res\drawable\bg_category_selected.xml +============================================================ + + + + + + +============================================================ +FILSTI: app\src\main\res\drawable\bg_category_unselected.xml +============================================================ + + + + + + + ============================================================ FILSTI: app\src\main\res\drawable\ic_book.xml ============================================================ @@ -3509,6 +4599,132 @@ FILSTI: app\src\main\res\layout\activity_main.xml +============================================================ +FILSTI: app\src\main\res\layout\bottom_sheet_calendar_details.xml +============================================================ + + + + + + + + + + + +