Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Web/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="sv">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>OpenHR</title>
<base href="/" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
Expand All @@ -15,7 +15,8 @@
<link rel="stylesheet" href="css/performance.css" />
<link rel="stylesheet" href="css/pwa-enhancements.css" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0e7490" />
<meta name="theme-color" content="#0e7490" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="OpenHR" />
Expand Down
14 changes: 12 additions & 2 deletions src/Web/Components/Layout/AdminLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
<MudLayout>
<TopBar UserName="@(Auth.UserName!)" UnreadCount="5"
IsDarkMode="_isDarkMode"
IsDarkModeChanged="OnDarkModeToggled" />
IsDarkModeChanged="OnDarkModeToggled"
OnHamburgerClick="ToggleDrawer" />
@* Responsive: temporär overlay-drawer under Md (960px), persistent på desktop.
Hamburger-knappen i TopBar öppnar/stänger den. *@
<MudDrawer @bind-Open="_drawerOpen" Variant="DrawerVariant.Responsive"
Elevation="1" ClipMode="DrawerClipMode.Always"
Breakpoint="Breakpoint.Lg">
Breakpoint="Breakpoint.Md">
<NavMenu />
</MudDrawer>
<MudMainContent Class="pt-16" id="main-content" role="main">
Expand All @@ -49,6 +52,8 @@
}
</MudContainer>
</MudMainContent>
@* Bottennavigering — visas endast på mobil (< 960px) via CSS. *@
<BottomNav UnreadCount="5" />
<RegionHR.Web.Components.Shared.OhrAssistant />
</MudLayout>
}
Expand All @@ -62,6 +67,11 @@
private bool _drawerOpen = true;
private bool _isDarkMode;

private void ToggleDrawer()
{
_drawerOpen = !_drawerOpen;
}

protected override void OnInitialized()
{
Nav.LocationChanged += OnLocationChanged;
Expand Down
39 changes: 39 additions & 0 deletions src/Web/Components/Layout/BottomNav.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@namespace RegionHR.Web.Components.Layout
@using Microsoft.AspNetCore.Components.Routing

@* ────────────────────────────────────────────────────────────
Bottennavigering för mobil. Döljs på desktop via CSS
(.ohr-bottom-nav i pwa-enhancements.css, display:none som default,
display:flex under 960px). Ger snabb åtkomst till de vanligaste
anställd-flödena. NavLink lägger på klassen "active" automatiskt.
──────────────────────────────────────────────────────────── *@
<nav class="ohr-bottom-nav" aria-label="Snabbnavigering">
<NavLink class="ohr-bottom-nav-item" href="/" Match="NavLinkMatch.All">
<MudIcon Icon="@Icons.Material.Filled.Home" Class="ohr-bottom-nav-icon" Size="Size.Medium" />
<span class="ohr-bottom-nav-label">Start</span>
</NavLink>
<NavLink class="ohr-bottom-nav-item" href="/minsida" Match="NavLinkMatch.Prefix">
<MudIcon Icon="@Icons.Material.Filled.AccountCircle" Class="ohr-bottom-nav-icon" Size="Size.Medium" />
<span class="ohr-bottom-nav-label">Min sida</span>
</NavLink>
<NavLink class="ohr-bottom-nav-item" href="/minsida/schema" Match="NavLinkMatch.All">
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Class="ohr-bottom-nav-icon" Size="Size.Medium" />
<span class="ohr-bottom-nav-label">Schema</span>
</NavLink>
<NavLink class="ohr-bottom-nav-item" href="/ledighet" Match="NavLinkMatch.All">
<MudIcon Icon="@Icons.Material.Filled.BeachAccess" Class="ohr-bottom-nav-icon" Size="Size.Medium" />
<span class="ohr-bottom-nav-label">Ledighet</span>
</NavLink>
<NavLink class="ohr-bottom-nav-item" href="/notiser" Match="NavLinkMatch.Prefix">
<MudIcon Icon="@Icons.Material.Filled.Notifications" Class="ohr-bottom-nav-icon" Size="Size.Medium" />
@if (UnreadCount > 0)
{
<span class="ohr-badge">@(UnreadCount > 99 ? "99+" : UnreadCount.ToString())</span>
}
<span class="ohr-bottom-nav-label">Notiser</span>
</NavLink>
</nav>

@code {
[Parameter] public int UnreadCount { get; set; }
}
28 changes: 18 additions & 10 deletions src/Web/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,26 @@
<MudNavLink Href="/kompetens/endorsements">@L["Endorsements"]</MudNavLink>
</MudNavGroup>

@* Självservice: förmåner, utbildning och resor är nåbara för ALLA inloggade
(RouteAccessPolicy: /formaner/*, /utbildning, /resor = AllaInloggade). Låg tidigare
gömda i chefsblocket → en anställd saknade menyväg till sina egna förmåner. *@
<MudNavGroup Title="@L["Benefits"]" Icon="@Icons.Material.Filled.CardGiftcard"
Expanded="@_formanerOpen" ExpandedChanged="@(v => _formanerOpen = v)">
<MudNavLink Href="/formaner">@L["BenefitsOverview"]</MudNavLink>
<MudNavLink Href="/formaner/enrollment">@L["BenefitsEnrollment"]</MudNavLink>
<MudNavLink Href="/formaner/val">@L["OpenEnrollment"]</MudNavLink>
<MudNavLink Href="/formaner/livshandelse">@L["LifeEvent"]</MudNavLink>
<MudNavLink Href="/formaner/sammanstallning">@L["BenefitsSummary"]</MudNavLink>
</MudNavGroup>
<MudNavLink Href="/utbildning" Icon="@Icons.Material.Filled.School">Utbildning</MudNavLink>
<MudNavLink Href="/resor" Icon="@Icons.Material.Filled.Flight">Resor</MudNavLink>

@if (Auth.IsInitialized && Auth.IsChef)
{
<MudNavLink Href="/godkannanden" Icon="@Icons.Material.Filled.CheckCircle">@L["Approvals"]</MudNavLink>
<MudNavLink Href="/journeys" Icon="@Icons.Material.Filled.Route">@L["EmployeeJourneys"]</MudNavLink>
<MudNavLink Href="/halsosam" Icon="@Icons.Material.Filled.LocalHospital">@L["HalsoSAM"]</MudNavLink>
<MudNavLink Href="/arbetsmiljo" Icon="@Icons.Material.Filled.Shield">@L["WorkEnvironment"]</MudNavLink>
<MudNavLink Href="/las" Icon="@Icons.Material.Filled.Warning">@L["LAS"]</MudNavLink>
<MudNavLink Href="/dokument" Icon="@Icons.Material.Filled.Description">@L["Documents"]</MudNavLink>
<MudNavLink Href="/medarbetarsamtal" Icon="@Icons.Material.Filled.RecordVoiceOver">@L["PerformanceReviews"]</MudNavLink>
<MudNavLink Href="/chef/1-1" Icon="@Icons.Material.Filled.Forum">1:1-möten</MudNavLink>
Expand All @@ -89,14 +102,6 @@
<MudNavLink Href="/rekrytering/vakanser">@L["Vacancies"]</MudNavLink>
<MudNavLink Href="/rekrytering/talangpool">@L["TalentPool"]</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="@L["Benefits"]" Icon="@Icons.Material.Filled.CardGiftcard"
Expanded="@_formanerOpen" ExpandedChanged="@(v => _formanerOpen = v)">
<MudNavLink Href="/formaner">@L["BenefitsOverview"]</MudNavLink>
<MudNavLink Href="/formaner/enrollment">@L["BenefitsEnrollment"]</MudNavLink>
<MudNavLink Href="/formaner/val">@L["OpenEnrollment"]</MudNavLink>
<MudNavLink Href="/formaner/livshandelse">@L["LifeEvent"]</MudNavLink>
<MudNavLink Href="/formaner/sammanstallning">@L["BenefitsSummary"]</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="@L["Reports"]" Icon="@Icons.Material.Filled.BarChart"
Expanded="@_rapporterOpen" ExpandedChanged="@(v => _rapporterOpen = v)">
<MudNavLink Href="/rapporter">@L["ReportLibrary"]</MudNavLink>
Expand All @@ -105,7 +110,6 @@
<MudNavLink Href="/rapporter/prediktion">@L["PredictiveAnalysis"]</MudNavLink>
<MudNavLink Href="/rapporter/workforce-plan">@L["WorkforcePlanning"]</MudNavLink>
<MudNavLink Href="/rapporter/flight-risk">@L["FlightRisk"]</MudNavLink>
<MudNavLink Href="/rapporter/lonetransparens">@L["PayTransparency"]</MudNavLink>
<MudNavLink Href="/rapporter/scenarioplanering">@L["ScenarioPlanning"]</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="@L["ContractStaff"]" Icon="@Icons.Material.Filled.GroupWork"
Expand Down Expand Up @@ -152,6 +156,10 @@
<MudNavLink Href="/admin/formaner/perioder">@L["EnrollmentPeriods"]</MudNavLink>
</MudNavGroup>
<MudNavLink Href="/admin/ona" Icon="@Icons.Material.Filled.Hub">Nätverksanalys (ONA)</MudNavLink>
@* HR/Admin-only: LAS-hantering och lönetransparens (flyttade hit ur chefsblocket —
chef nekas dessa i RouteAccessPolicy och ska därför inte se dem i menyn). *@
<MudNavLink Href="/las" Icon="@Icons.Material.Filled.Warning">@L["LAS"]</MudNavLink>
<MudNavLink Href="/rapporter/lonetransparens" Icon="@Icons.Material.Filled.Insights">@L["PayTransparency"]</MudNavLink>
<MudNavLink Href="/gdpr" Icon="@Icons.Material.Filled.Security">@L["GDPR"]</MudNavLink>
<MudNavLink Href="/audit" Icon="@Icons.Material.Filled.History">@L["AuditLog"]</MudNavLink>
}
Expand Down
8 changes: 7 additions & 1 deletion src/Web/Components/Layout/Shared/TopBar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
@using RegionHR.Web

<MudAppBar Fixed="true" Color="Color.Primary" Elevation="1">
@if (OnHamburgerClick.HasDelegate)
{
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start"
OnClick="OnHamburgerClick" aria-label="Växla sidomeny" />
}
<MudText Typo="Typo.h6" Class="ml-3" Style="font-weight: 700; cursor: pointer;" @onclick="@(() => Nav?.NavigateTo("/"))">OpenHR</MudText>

<div style="margin-left: 1rem; max-width: 320px; flex: 1;">
<div class="ohr-appbar-search" style="margin-left: 1rem; max-width: 320px; flex: 1;">
<MudAutocomplete T="string" Placeholder="@L["Search"]"
SearchFunc="Search" Variant="Variant.Filled" Dense="true"
Style="background: rgba(255,255,255,0.15); border-radius: 8px;"
Expand Down Expand Up @@ -74,6 +79,7 @@
[Parameter] public int UnreadCount { get; set; }
[Parameter] public bool IsDarkMode { get; set; }
[Parameter] public EventCallback<bool> IsDarkModeChanged { get; set; }
[Parameter] public EventCallback OnHamburgerClick { get; set; }

private string _currentLang = "sv";
private List<string> _recentSearches = new();
Expand Down
16 changes: 10 additions & 6 deletions src/Web/Components/Pages/Chef/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,16 @@ else if (!_loading)
? franvaroAll.Count
: franvaroAll.Count(r => scopeIds.Contains(r.AnstallId));

// Coaching nudges
_nudges = await db.CoachingNudges
.Where(n => !n.ArLast)
.OrderByDescending(n => n.SkapadVid)
.Take(5)
.ToListAsync();
// Coaching nudges — bara den inloggade chefens egna (CoachingNudge.ChefId),
// aldrig en annan chefs insikter. Admin/HR utan chefskoppling får inga.
if (Auth.EmployeeId is Guid chefId)
{
_nudges = await db.CoachingNudges
.Where(n => !n.ArLast && n.ChefId == chefId)
.OrderByDescending(n => n.SkapadVid)
.Take(5)
.ToListAsync();
}
}
catch (Exception ex) { _error = $"Kunde inte hämta data: {ex.Message}"; }
finally { _loading = false; }
Expand Down
63 changes: 54 additions & 9 deletions src/Web/Components/Pages/Chef/OneOnOne.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
@layout RegionHR.Web.Components.Layout.AdminLayout
@using RegionHR.Infrastructure.Persistence
@using RegionHR.Performance.Domain
@using RegionHR.SharedKernel.Domain
@using RegionHR.Web.Services
@using Microsoft.EntityFrameworkCore
@inject IDbContextFactory<RegionHRDbContext> DbFactory
@inject AuthService Auth
@inject UnitScopeService Scope
@inject ISnackbar Snackbar

<PageTitle>OpenHR - 1:1-möten</PageTitle>
Expand Down Expand Up @@ -37,8 +41,12 @@
<MudTextField Label="Agenda (valfri)" @bind-Value="_nyAgenda" Variant="Variant.Outlined" />
</MudItem>
</MudGrid>
@if (Auth.EmployeeId == null)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-3">Bokning kräver en kopplad chefsprofil — logga in med en chef.</MudAlert>
}
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-3" Style="min-height: 44px;"
OnClick="SkapaMote" Disabled="@(_nyAnstallId == Guid.Empty)">Boka möte</MudButton>
OnClick="SkapaMote" Disabled="@(_nyAnstallId == Guid.Empty || Auth.EmployeeId == null)">Boka möte</MudButton>
</MudPaper>

@* === UPCOMING MEETINGS === *@
Expand Down Expand Up @@ -148,7 +156,17 @@ else
await using var db = await DbFactory.CreateDbContextAsync();
try
{
var emps = await db.Employees.OrderBy(e => e.Efternamn).Take(200).ToListAsync();
// Medarbetar-listan scopas till chefens EGET team (UnitScopeService).
// En chef ska bara kunna boka 1:1 med sina egna medarbetare — inte hela
// organisationen. HR/Admin (null scope) ser alla, som tidigare.
var scopeIds = await Scope.GetEmployeeIdsInScopeAsync();
IQueryable<RegionHR.Core.Domain.Employee> empQuery = db.Employees;
if (scopeIds is not null)
{
var scopeEmpIds = scopeIds.Select(g => EmployeeId.From(g)).ToList();
empQuery = empQuery.Where(e => scopeEmpIds.Contains(e.Id));
}
var emps = await empQuery.OrderBy(e => e.Efternamn).Take(500).ToListAsync();
_employees = emps.Select(e => (e.Id.Value, $"{e.Fornamn} {e.Efternamn}")).ToList();
_employeeNames = emps.ToDictionary(e => e.Id.Value, e => $"{e.Fornamn} {e.Efternamn}");

Expand All @@ -160,29 +178,56 @@ else
private async Task LaddaData()
{
await using var db = await DbFactory.CreateDbContextAsync();
var now = DateTime.UtcNow;
_upcoming = await db.OneOnOneMeetings

// Möten scopas till den inloggade chefens egna (OneOnOneMeeting.ChefId).
// Tidigare visades ALLA chefers 1:1-möten och deras privata anteckningar.
// Admin utan kopplad profil (ChefId saknas) faller tillbaka på att visa alla.
IQueryable<OneOnOneMeeting> baseQ = db.OneOnOneMeetings;
if (Auth.EmployeeId is Guid chefId)
baseQ = baseQ.Where(m => m.ChefId == chefId);

_upcoming = await baseQ
.Where(m => m.Status == MeetingStatus.Scheduled)
.OrderBy(m => m.Datum).Take(50).ToListAsync();

_past = await db.OneOnOneMeetings
_past = await baseQ
.Where(m => m.Status != MeetingStatus.Scheduled)
.OrderByDescending(m => m.Datum).Take(50).ToListAsync();

_actionItems = await db.MeetingActionItems
.Where(a => a.Status != ActionItemStatus.Done)
.OrderBy(a => a.Deadline).Take(50).ToListAsync();
// Åtgärdspunkter begränsas till chefens egna möten.
if (Auth.EmployeeId is Guid cid)
{
var myMeetingIds = await db.OneOnOneMeetings
.Where(m => m.ChefId == cid).Select(m => m.Id).ToListAsync();
_actionItems = await db.MeetingActionItems
.Where(a => a.Status != ActionItemStatus.Done && myMeetingIds.Contains(a.MeetingId))
.OrderBy(a => a.Deadline).Take(50).ToListAsync();
}
else
{
_actionItems = await db.MeetingActionItems
.Where(a => a.Status != ActionItemStatus.Done)
.OrderBy(a => a.Deadline).Take(50).ToListAsync();
}
}

private string GetNamn(Guid id) => _employeeNames.GetValueOrDefault(id, "Okänd");

private async Task SkapaMote()
{
// Mötet måste ägas av den inloggade chefen (ChefId) — annars går det inte
// att scopa listan till "mina möten". Tidigare sattes ett slumpmässigt
// Guid.NewGuid() som chef, vilket frikopplade varje möte från sin chef.
if (Auth.EmployeeId is not Guid chefId)
{
_error = "Bokning kräver en kopplad chefsprofil — logga in med en chef.";
return;
}
await using var db = await DbFactory.CreateDbContextAsync();
try
{
var datum = _nyDatum ?? DateTime.Today.AddDays(7);
var meeting = OneOnOneMeeting.Skapa(Guid.NewGuid(), _nyAnstallId, datum, _nyAgenda);
var meeting = OneOnOneMeeting.Skapa(chefId, _nyAnstallId, datum, _nyAgenda);
await db.OneOnOneMeetings.AddAsync(meeting);
await db.SaveChangesAsync();
Snackbar.Add("Möte bokat!", Severity.Success);
Expand Down
27 changes: 18 additions & 9 deletions src/Web/Components/Pages/Chef/Scorecard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
@layout RegionHR.Web.Components.Layout.AdminLayout
@using RegionHR.Infrastructure.Persistence
@using RegionHR.Performance.Domain
@using RegionHR.Web.Services
@using Microsoft.EntityFrameworkCore
@inject IDbContextFactory<RegionHRDbContext> DbFactory
@inject AuthService Auth

<PageTitle>OpenHR - Chefsstyrkort</PageTitle>

Expand Down Expand Up @@ -116,16 +118,23 @@ else
await using var db = await DbFactory.CreateDbContextAsync();
try
{
// Get latest scorecard (in real app, filter by logged-in chef)
_scorecard = await db.ManagerScorecards
.OrderByDescending(s => s.GenereradVid)
.FirstOrDefaultAsync();
// Styrkortet är chefs-specifikt (ManagerScorecard.ChefId). Visa ENDAST
// den inloggade chefens eget styrkort och egna coaching-insikter — aldrig
// en annan chefs (tidigare togs "senast genererade" oavsett chef).
// HR/Admin utan egen chefskoppling ser inget styrkort (de leder inget team här).
if (Auth.EmployeeId is Guid chefId)
{
_scorecard = await db.ManagerScorecards
.Where(s => s.ChefId == chefId)
.OrderByDescending(s => s.GenereradVid)
.FirstOrDefaultAsync();

_nudges = await db.CoachingNudges
.Where(n => !n.ArLast)
.OrderByDescending(n => n.SkapadVid)
.Take(20)
.ToListAsync();
_nudges = await db.CoachingNudges
.Where(n => !n.ArLast && n.ChefId == chefId)
.OrderByDescending(n => n.SkapadVid)
.Take(20)
.ToListAsync();
}
}
catch (Exception ex) { _error = $"Kunde inte hämta data: {ex.Message}"; }
finally { _loading = false; }
Expand Down
Loading
Loading