diff --git a/backend/app/config.py b/backend/app/config.py
index 2d142ad..ecd1acb 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -37,11 +37,15 @@ class LeaveRules(BaseModel):
max_savable_days_at_age_4: int = 96 # age-4 ceiling — per CHILD (pooled across parents)
hard_expiry_age_years: int = 12
- # A couple's per-parent age-4 pooling-loss tip (see
- # `app.utils.tip_predicates.age4_pooling_loss`) only starts warning once
- # the child has turned this many years old — the first couple of years
- # are too early to usefully act on, so don't nag before then.
- age4_pooling_tip_min_child_age_years: int = 2
+ # A couple's age-4 pooling-loss tip (see
+ # `app.utils.tip_predicates.age4_pooling_loss`) only fires once the
+ # calendar holds at least this many planned parental-benefit days for the
+ # child. The calendar IS the plan: below this the projection is mostly
+ # untouched entitlement rather than a real plan, so the "loss" is an
+ # artefact of not having planned yet, not something to act on. Child age is
+ # deliberately NOT the gate — a family that plans in detail early should get
+ # the advice early. A tunable threshold, not a legal rule.
+ age4_pooling_tip_min_planned_days: int = 20
sgi_protected_first_months: int = 12
sgi_min_days_per_week_after_age_1: int = 5
diff --git a/backend/app/services/tips_service.py b/backend/app/services/tips_service.py
index 46cf937..af40800 100644
--- a/backend/app/services/tips_service.py
+++ b/backend/app/services/tips_service.py
@@ -180,6 +180,7 @@ async def _rule_preserve_for_age_4(ctx: TipsContext) -> Tip | None:
"parent_name": parent.name,
"expiring_days": expiring_at_b4,
"fourth_birthday": fourth_bday.isoformat(),
+ "parent_cap": savable_cap,
},
anchor_date=fourth_bday,
)
@@ -196,6 +197,19 @@ async def _rule_age4_pooling_loss(ctx: TipsContext) -> Tip | None:
can cause) is a legal fact about the child having two parents, purely a
function of parent count.
+ The gate is the size of the plan, not the child's age: below
+ `settings.rules.age4_pooling_tip_min_planned_days` planned
+ parental-benefit days for this child, the projection this rule reasons
+ over is mostly untouched entitlement rather than anything the family
+ actually decided — the "loss" would be an artefact of not having planned
+ yet, not something to act on. Child age is deliberately not the gate: a
+ family that plans in detail early (e.g. right after birth) should get
+ this advice early, not be made to wait out an arbitrary age. Once the
+ calendar holds a real plan, severity is `info` rather than `warning` —
+ the dashboard hero already carries the alarm about days expiring, so this
+ card reads as forward-looking advice on a four-year-out projection, not a
+ second verdict on the user's planning.
+
Advice order is the substance of this tip: taking more leave before the
birthday converts entitlement into actual time at home (strictly
better), so it is offered first. Transferring days at Försäkringskassan
@@ -210,8 +224,9 @@ async def _rule_age4_pooling_loss(ctx: TipsContext) -> Tip | None:
return None
child = ctx.children[0]
- if not is_child_over_age(child.birth_date, settings.rules.age4_pooling_tip_min_child_age_years):
- return None # first couple of years: too early to usefully act on this
+ planned_fp_count = sum(1 for d in ctx.planned_days if d.day_type in ("sjukpenning", "lagsta"))
+ if planned_fp_count < settings.rules.age4_pooling_tip_min_planned_days:
+ return None # not enough of a real plan yet for this projection to mean anything
fourth_bday = child.birth_date + _relativedelta(years=4)
per_parent_cap = resolve_age4_savable_cap(len(ctx.parents))
@@ -269,7 +284,7 @@ async def _rule_age4_pooling_loss(ctx: TipsContext) -> Tip | None:
transfer_days = max(transfer_days, 0)
return Tip(
id="age4_pooling_loss",
- severity="warning",
+ severity="info",
message_key="tips.age4_pooling_loss_exact",
data={
"surplus_parent": surplus_parent.name,
@@ -285,7 +300,7 @@ async def _rule_age4_pooling_loss(ctx: TipsContext) -> Tip | None:
if not partner_has_snapshot and other_parents:
return Tip(
id="age4_pooling_loss",
- severity="warning",
+ severity="info",
message_key="tips.age4_pooling_loss_hedged",
data={
"surplus_parent": surplus_parent.name,
@@ -634,9 +649,20 @@ class TipsService:
for ctx in child_contexts:
child = ctx.children[0]
results = await asyncio.gather(*(rule(ctx) for rule in child_rules))
- for tip in results:
- if tip is None:
- continue
+ batch = [tip for tip in results if tip is not None]
+
+ # `age4_pooling_loss` is a strict superset of `preserve_for_age_4`
+ # for a given child: it states the same expiring-days loss *and*
+ # the remedy (take more leave, then transfer) *and* the correct
+ # per-parent cap, while `preserve_for_age_4` only ever names one
+ # parent's figure against a cap the reader may misread as the
+ # per-child 96. Emitting both is duplicate, mutually contradictory
+ # advice (two different "you're losing N days" numbers), so drop
+ # the weaker one whenever the stronger one also fired this batch.
+ if any(tip.id == "age4_pooling_loss" for tip in batch):
+ batch = [tip for tip in batch if tip.id != "preserve_for_age_4"]
+
+ for tip in batch:
tip.data = {**tip.data, "child_id": str(child.id), "child_name": child.name}
tip.id = f"{tip.id}:{child.id}"
tips.append(tip)
diff --git a/backend/tests/unit/test_services/test_tips_service.py b/backend/tests/unit/test_services/test_tips_service.py
index f74fdd7..6e3581a 100644
--- a/backend/tests/unit/test_services/test_tips_service.py
+++ b/backend/tests/unit/test_services/test_tips_service.py
@@ -9,6 +9,7 @@ from uuid import UUID, uuid4
from dateutil.relativedelta import relativedelta
+from app.config import settings
from app.entities.child import Child
from app.entities.leave_wish import LeaveWish
from app.entities.parental_leave import ParentalLeaveFetch, TierData
@@ -274,6 +275,7 @@ class TestPreserveForAge4:
assert tip.id == "preserve_for_age_4"
assert tip.severity == "warning"
assert tip.data["expiring_days"] == 124
+ assert tip.data["parent_cap"] == 96
async def test_no_fire_when_projected_days_within_savable_limit(self):
ws = uuid4()
@@ -340,6 +342,7 @@ class TestPreserveForAge4:
tip = await _rule_preserve_for_age_4(ctx)
assert tip is not None
assert tip.data["expiring_days"] == 172 # 220 - 48
+ assert tip.data["parent_cap"] == 48
# ---------------------------------------------------------------------------
@@ -347,6 +350,24 @@ class TestPreserveForAge4:
# ---------------------------------------------------------------------------
+def _make_fp_planned_days(child_id: UUID, workspace_id: UUID, count: int) -> list[PlannedLeaveDay]:
+ """`count` planned FP days attributed to a parent NOT in the test's
+
+ `ctx.parents`, so they clear `age4_pooling_tip_min_planned_days`'s
+ "is there a real plan" gate (a workspace-wide count) without being
+ attributable to either tested parent and shifting their
+ `planned_fp_before_b4` figures — which would silently change the
+ excess/projected-left numbers the exact-figure assertions below depend
+ on.
+ """
+ filler_parent = _make_parent(workspace_id=workspace_id, name="Filler")
+ base = date.today() - relativedelta(years=10)
+ return [
+ _make_fp_day(filler_parent, child_id, workspace_id, base - timedelta(days=i))
+ for i in range(count)
+ ]
+
+
class TestAge4PoolingLoss:
async def test_fires_for_60_10_couple_case_with_exact_figures(self):
"""The headline scenario: A has 60 days left, B has 10 (cap 48 each).
@@ -364,16 +385,20 @@ class TestAge4PoolingLoss:
b = _make_parent(workspace_id=ws, name="B")
leave_a = _make_leave(a, child.id, total=240, used=180) # left = 60
leave_b = _make_leave(b, child.id, total=240, used=230) # left = 10
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days
+ )
ctx = _make_ctx(
workspace_id=ws,
children=[child],
parents=[a, b],
leave_by_parent={a.uuid: leave_a, b.uuid: leave_b},
+ planned_days=planned,
)
tip = await _rule_age4_pooling_loss(ctx)
assert tip is not None
assert tip.id == "age4_pooling_loss"
- assert tip.severity == "warning"
+ assert tip.severity == "info"
assert tip.message_key == "tips.age4_pooling_loss_exact"
assert tip.data["surplus_parent"] == "A"
assert tip.data["lagging_parent"] == "B"
@@ -394,11 +419,15 @@ class TestAge4PoolingLoss:
b = _make_parent(workspace_id=ws, name="B")
leave_a = _make_leave(a, child.id, total=240, used=180, transferable_days_left=5)
leave_b = _make_leave(b, child.id, total=240, used=230)
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days
+ )
ctx = _make_ctx(
workspace_id=ws,
children=[child],
parents=[a, b],
leave_by_parent={a.uuid: leave_a, b.uuid: leave_b},
+ planned_days=planned,
)
tip = await _rule_age4_pooling_loss(ctx)
assert tip is not None
@@ -414,11 +443,15 @@ class TestAge4PoolingLoss:
a = _make_parent(workspace_id=ws, name="A")
b = _make_parent(workspace_id=ws, name="B")
leave_a = _make_leave(a, child.id, total=240, used=180) # left = 60
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days
+ )
ctx = _make_ctx(
workspace_id=ws,
children=[child],
parents=[a, b],
leave_by_parent={a.uuid: leave_a, b.uuid: None},
+ planned_days=planned,
)
tip = await _rule_age4_pooling_loss(ctx)
assert tip is not None
@@ -435,31 +468,73 @@ class TestAge4PoolingLoss:
b = _make_parent(workspace_id=ws, name="B")
leave_a = _make_leave(a, child.id, total=240, used=200) # left = 40
leave_b = _make_leave(b, child.id, total=240, used=210) # left = 30
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days
+ )
ctx = _make_ctx(
workspace_id=ws,
children=[child],
parents=[a, b],
leave_by_parent={a.uuid: leave_a, b.uuid: leave_b},
+ planned_days=planned,
)
tip = await _rule_age4_pooling_loss(ctx)
assert tip is None
- async def test_no_fire_for_child_under_2(self):
+ async def test_no_fire_below_planned_day_threshold(self):
+ """Same 60/10 surplus shape as the headline case, but the calendar
+
+ holds one planned FP day short of the threshold — the rule must not
+ fire on an entitlement projection that isn't backed by a real plan
+ yet, regardless of how large the eventual loss would be.
+ """
ws = uuid4()
- child = _make_child(date.today() - relativedelta(years=1))
+ child = _make_child(date.today() - relativedelta(years=3))
a = _make_parent(workspace_id=ws, name="A")
b = _make_parent(workspace_id=ws, name="B")
leave_a = _make_leave(a, child.id, total=240, used=180) # left = 60
leave_b = _make_leave(b, child.id, total=240, used=230) # left = 10
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days - 1
+ )
ctx = _make_ctx(
workspace_id=ws,
children=[child],
parents=[a, b],
leave_by_parent={a.uuid: leave_a, b.uuid: leave_b},
+ planned_days=planned,
)
tip = await _rule_age4_pooling_loss(ctx)
assert tip is None
+ async def test_fires_for_infant_once_the_calendar_holds_a_real_plan(self):
+ """Child age is no longer the gate: an infant with a substantive
+
+ plan already in the calendar (>= the planned-day threshold) must get
+ this advice just as readily as an older child — this is the
+ regression that proves the gate moved from child age to plan size.
+ """
+ ws = uuid4()
+ child = _make_child(date.today() - relativedelta(months=3))
+ a = _make_parent(workspace_id=ws, name="A")
+ b = _make_parent(workspace_id=ws, name="B")
+ leave_a = _make_leave(a, child.id, total=240, used=180) # left = 60
+ leave_b = _make_leave(b, child.id, total=240, used=230) # left = 10
+ planned = _make_fp_planned_days(
+ child.id, ws, settings.rules.age4_pooling_tip_min_planned_days
+ )
+ ctx = _make_ctx(
+ workspace_id=ws,
+ children=[child],
+ parents=[a, b],
+ leave_by_parent={a.uuid: leave_a, b.uuid: leave_b},
+ planned_days=planned,
+ )
+ tip = await _rule_age4_pooling_loss(ctx)
+ assert tip is not None
+ assert tip.message_key == "tips.age4_pooling_loss_exact"
+ assert tip.data["transfer_days"] == 12
+
# ---------------------------------------------------------------------------
# _rule_use_dubbeldagar_strategically
diff --git a/backend/tests/unit/test_services/test_tips_service_context.py b/backend/tests/unit/test_services/test_tips_service_context.py
index 1923fd9..ef497b3 100644
--- a/backend/tests/unit/test_services/test_tips_service_context.py
+++ b/backend/tests/unit/test_services/test_tips_service_context.py
@@ -235,6 +235,109 @@ class TestMultiChildContexts:
assert child_names == {"Adam", "Berit"}
+class TestPoolingLossSuppressesPreserveForAge4:
+ """`age4_pooling_loss` is a strict superset of `preserve_for_age_4` for a
+
+ given child (same expiring-days loss, plus the remedy and the correct
+ per-parent cap), so `TipsService.get_tips` drops the weaker
+ `preserve_for_age_4` whenever the stronger tip also fires in the same
+ per-child batch — see the suppression logic in `get_tips`.
+ """
+
+ async def test_two_parent_surplus_headroom_suppresses_preserve_for_age_4(
+ self,
+ tips_service: TipsService,
+ workspace_repo: SQLiteWorkspaceRepository,
+ personal_data_repo: SQLitePersonalDataRepository,
+ child_repo: SQLiteChildRepository,
+ parental_leave_repo: SQLiteParentalLeaveRepository,
+ planned_leave_repo: SQLitePlannedLeaveRepository,
+ ) -> None:
+ now = datetime.now()
+ ws = await workspace_repo.create(Workspace(id=uuid4(), name="Familj", created_at=now))
+ parent_a = await personal_data_repo.create(
+ PersonalData(uuid=uuid4(), workspace_id=ws.id, name="A", created_at=now)
+ )
+ parent_b = await personal_data_repo.create(
+ PersonalData(uuid=uuid4(), workspace_id=ws.id, name="B", created_at=now)
+ )
+ child = await child_repo.create(
+ Child(
+ id=uuid4(),
+ workspace_id=ws.id,
+ name="Barn",
+ birth_date=date.today().replace(year=date.today().year - 1),
+ created_at=now,
+ )
+ )
+ fourth_bday = child.birth_date.replace(year=child.birth_date.year + 4)
+
+ # A: left = 60 (cap 48 -> excess 12). B: left = 10 (cap 48 -> headroom
+ # 38). Pooled the family could keep min(70, 96) = 70; capped per
+ # parent it only keeps 48 + 10 = 58 -> family_loss = 12.
+ await parental_leave_repo.create(_make_fetch(parent_a.uuid, ws.id, child.id, used=180))
+ await parental_leave_repo.create(_make_fetch(parent_b.uuid, ws.id, child.id, used=230))
+
+ # A substantive plan (>= the planned-day gate), dated AFTER the 4th
+ # birthday so it doesn't shift `planned_fp_before_b4` and change the
+ # expiring/excess figures computed above.
+ for i in range(20):
+ await planned_leave_repo.create(
+ _make_planned_day(
+ ws.id, child.id, parent_a.uuid, fourth_bday + timedelta(days=i + 30)
+ )
+ )
+
+ tips = await tips_service.get_tips(ws.id)
+
+ pooling_tips = [t for t in tips if t.id == f"age4_pooling_loss:{child.id}"]
+ assert len(pooling_tips) == 1, "expected age4_pooling_loss to fire for this shape"
+
+ preserve_tips = [t for t in tips if t.id.startswith("preserve_for_age_4:")]
+ assert preserve_tips == [], (
+ "preserve_for_age_4 must be suppressed when age4_pooling_loss fires "
+ f"for the same child, got {[t.id for t in preserve_tips]}"
+ )
+
+ async def test_single_parent_over_cap_keeps_preserve_for_age_4(
+ self,
+ tips_service: TipsService,
+ workspace_repo: SQLiteWorkspaceRepository,
+ personal_data_repo: SQLitePersonalDataRepository,
+ child_repo: SQLiteChildRepository,
+ parental_leave_repo: SQLiteParentalLeaveRepository,
+ ) -> None:
+ """A single-parent workspace never has 2+ parents, so
+
+ `age4_pooling_loss` never even runs (it early-returns on
+ `len(ctx.parents) < 2`) — nothing suppresses `preserve_for_age_4`.
+ """
+ now = datetime.now()
+ ws = await workspace_repo.create(Workspace(id=uuid4(), name="Familj", created_at=now))
+ parent = await personal_data_repo.create(
+ PersonalData(uuid=uuid4(), workspace_id=ws.id, name="Solo", created_at=now)
+ )
+ child = await child_repo.create(
+ Child(
+ id=uuid4(),
+ workspace_id=ws.id,
+ name="Barn",
+ birth_date=date.today().replace(year=date.today().year - 1),
+ created_at=now,
+ )
+ )
+ # total=240, used=20 -> projected_left=220; single-parent cap=96 ->
+ # expiring=124 > 0.
+ await parental_leave_repo.create(_make_fetch(parent.uuid, ws.id, child.id, used=20))
+
+ tips = await tips_service.get_tips(ws.id)
+
+ preserve_tips = [t for t in tips if t.id == f"preserve_for_age_4:{child.id}"]
+ assert len(preserve_tips) == 1
+ assert preserve_tips[0].data["expiring_days"] == 124
+ assert not any(t.id.startswith("age4_pooling_loss:") for t in tips)
+
+
class TestDismissals:
async def test_dismiss_hides_tip_until_data_changes(
self,
diff --git a/frontend/src/components/dashboard/Hero.test.tsx b/frontend/src/components/dashboard/Hero.test.tsx
index 3a2a6ba..bc9bb52 100644
--- a/frontend/src/components/dashboard/Hero.test.tsx
+++ b/frontend/src/components/dashboard/Hero.test.tsx
@@ -1,6 +1,8 @@
-import { describe, it, expect } from 'vitest'
+import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Hero } from './Hero'
+import { FK_TRANSFER_LOGIN_URL } from '@/utils/constants'
+import { LanguageProvider } from '@/i18n'
describe('Hero', () => {
it('uses the forward-looking framing when no plan exists yet', () => {
@@ -35,6 +37,123 @@ describe('Hero', () => {
expect(hero).not.toHaveAttribute('role', 'alert')
})
+ it('renders the remedy line when a plan exists, days are at risk, and a remedy is supplied', () => {
+ render(
+
+ {t('dashboard.hero.remedyLabel')}{' '} + {remedyBeforeLink} + {remedyAfterLink !== undefined ? ( + + {t('dashboard.hero.fkLinkText')} + + ) : null} + {remedyAfterLink} +
+ )} ) } diff --git a/frontend/src/i18n/en/dashboard.ts b/frontend/src/i18n/en/dashboard.ts index 820a3e3..05670e3 100644 --- a/frontend/src/i18n/en/dashboard.ts +++ b/frontend/src/i18n/en/dashboard.ts @@ -15,9 +15,12 @@ export const dashboard = { 'dashboard.figureExpiring': 'Expiring at age 4', 'dashboard.figureLeft': 'Days left', 'dashboard.figureUsed': 'Used', + 'dashboard.hero.fkLinkText': 'Försäkringskassan', 'dashboard.hero.hasPlanMessage': '{atRisk} days are at risk of disappearing when {childName} turns 4 ({date}).', 'dashboard.hero.noneAtRiskMessage': 'No days will be lost — your plan uses them all before {childName} turns 4 ({date}).', 'dashboard.hero.noPlanMessage': 'You have {days} days to plan. Without a plan, {atRisk} of them will disappear when {childName} turns 4.', + 'dashboard.hero.remedyLabel': 'How to save them', + 'dashboard.hero.remedyMessage': 'Take them as leave before then, or transfer up to {days} days from {fromParent} to {toParent} at {fk} before {date}.', 'dashboard.leaveOverviewTitle': 'Until age 4', 'dashboard.moreDetailsSummary': 'Details', 'dashboard.overviewTitle': 'Overview', diff --git a/frontend/src/i18n/en/tips.ts b/frontend/src/i18n/en/tips.ts index 2a4ef3b..459dbcf 100644 --- a/frontend/src/i18n/en/tips.ts +++ b/frontend/src/i18n/en/tips.ts @@ -1,10 +1,10 @@ export const tips = { 'tip.age4PoolingLossExact.body': - '{surplus_parent} has {excess_days} more days left than may be saved per parent at the child\'s 4th birthday ({fourth_birthday}) — otherwise the family loses {family_loss_days} days. Taking that leave before then is best, since it turns entitlement into actual time at home. If that\'s not possible, you can transfer up to {transfer_days} days to {lagging_parent} at Försäkringskassan instead — that saves the days but doesn\'t add any extra time off, and the transfer itself happens at Försäkringskassan, not in this app.', - 'tip.age4PoolingLossExact.title': 'Uneven saving costs the family days', + 'Plans change, and this is only a projection from the calendar as it stands today. If it holds, {surplus_parent} ends up with {excess_days} more days left than may be saved per parent at the child\'s 4th birthday ({fourth_birthday}) — the family would lose {family_loss_days} days. Taking that leave before then is best, since it turns entitlement into actual time at home. Otherwise you can transfer up to {transfer_days} days to {lagging_parent} at Försäkringskassan — that saves the days but doesn\'t add any extra time off, and the transfer itself happens at Försäkringskassan, not in this app. Keep your balances here up to date as you go, so this stays based on what you actually took.', + 'tip.age4PoolingLossExact.title': 'Days you can still save by transferring', 'tip.age4PoolingLossHedged.body': - '{surplus_parent} has {excess_days} more days left than may be saved per parent at the child\'s 4th birthday ({fourth_birthday}). Taking that leave before then is best, since it turns entitlement into actual time at home. If you both have days left, you can otherwise transfer days between you at Försäkringskassan — that saves the days but doesn\'t add any extra time off, and the transfer itself happens at Försäkringskassan, not in this app.', - 'tip.age4PoolingLossHedged.title': 'Uneven saving may cost the family days', + 'Plans change, and this is only a projection from the calendar as it stands today. If it holds, {surplus_parent} ends up with {excess_days} more days left than may be saved per parent at the child\'s 4th birthday ({fourth_birthday}). Taking that leave before then is best, since it turns entitlement into actual time at home. If you both have days left, you can otherwise transfer days between you at Försäkringskassan — that saves the days but doesn\'t add any extra time off, and the transfer itself happens at Försäkringskassan, not in this app. Keep your balances here up to date as you go, so this stays based on what you actually took.', + 'tip.age4PoolingLossHedged.title': 'Days you may be able to save by transferring', 'tip.aria': 'Tip: {title}', 'tip.balanceBetweenParents.body': '{leading_parent} has taken {gap_days} more days than {lagging_parent}. Consider evening out the split.', 'tip.balanceBetweenParents.title': 'Imbalance between parents', @@ -13,7 +13,7 @@ export const tips = { 'tip.lagstaNivaCoverage.title': 'Cover the gap with lägstanivå', 'tip.longBreakParentSwap.body': '{parent_name} is covering less than half of "{break_name}" ({start_date}–{end_date}). Let {other_parent_name} take part of the period.', 'tip.longBreakParentSwap.title': 'Consider alternating', - 'tip.preserveForAge4.body': '{parent_name} risks losing {expiring_days} days when the child turns 4 ({fourth_birthday}). Only 96 days can be saved after that.', + 'tip.preserveForAge4.body': '{parent_name} risks losing {expiring_days} days when the child turns 4 ({fourth_birthday}). Only {parent_cap} days can be saved per parent after that.', 'tip.preserveForAge4.title': 'Days at risk of being lost at age 4', 'tip.reserveForPreschoolPlanningDays.body': '{parent_name} has {days_left} days left, which barely covers the preschool\'s {buffer_days} planning days. Reserve days ahead of time.', 'tip.reserveForPreschoolPlanningDays.title': 'Reserve days for preschool planning days', diff --git a/frontend/src/i18n/sv/dashboard.ts b/frontend/src/i18n/sv/dashboard.ts index 4598aaf..f0aede3 100644 --- a/frontend/src/i18n/sv/dashboard.ts +++ b/frontend/src/i18n/sv/dashboard.ts @@ -15,9 +15,12 @@ export const dashboard = { 'dashboard.figureExpiring': 'Förfaller vid 4 år', 'dashboard.figureLeft': 'Dagar kvar', 'dashboard.figureUsed': 'Använda', + 'dashboard.hero.fkLinkText': 'Försäkringskassan', 'dashboard.hero.hasPlanMessage': '{atRisk} dagar riskerar att försvinna när {childName} fyller 4 ({date}).', 'dashboard.hero.noneAtRiskMessage': 'Inga dagar går förlorade — er plan hinner använda alla innan {childName} fyller 4 ({date}).', 'dashboard.hero.noPlanMessage': 'Du har {days} dagar att planera. Utan en plan försvinner {atRisk} av dem när {childName} fyller 4.', + 'dashboard.hero.remedyLabel': 'Så räddar ni dem', + 'dashboard.hero.remedyMessage': 'Ta ut dem som ledighet innan dess, eller för över upp till {days} dagar från {fromParent} till {toParent} hos {fk} före {date}.', 'dashboard.leaveOverviewTitle': 'Fram till 4 år', 'dashboard.moreDetailsSummary': 'Detaljer', 'dashboard.overviewTitle': 'Översikt', diff --git a/frontend/src/i18n/sv/tips.ts b/frontend/src/i18n/sv/tips.ts index 934f9af..8530088 100644 --- a/frontend/src/i18n/sv/tips.ts +++ b/frontend/src/i18n/sv/tips.ts @@ -1,10 +1,10 @@ export const tips = { 'tip.age4PoolingLossExact.body': - '{surplus_parent} har {excess_days} fler dagar kvar än vad som får sparas per förälder vid 4-årsdagen ({fourth_birthday}) — annars tappar familjen {family_loss_days} dagar. Bäst är att ta ut dagarna som ledighet innan dess, det ger faktisk tid hemma. Går inte det kan ni överföra upp till {transfer_days} dagar till {lagging_parent} hos Försäkringskassan istället — det räddar dagarna men ger ingen extra ledighet, och överföringen sker hos Försäkringskassan, inte i appen.', - 'tip.age4PoolingLossExact.title': 'Ojämnt sparande kostar familjen dagar', + 'Planer ändras, och det här är bara en prognos utifrån kalendern som den ser ut i dag. Om den håller hamnar {surplus_parent} med {excess_days} fler dagar kvar än vad som får sparas per förälder vid 4-årsdagen ({fourth_birthday}) — familjen skulle då tappa {family_loss_days} dagar. Bäst är att ta ut dagarna som ledighet innan dess, det ger faktisk tid hemma. Annars kan ni föra över upp till {transfer_days} dagar till {lagging_parent} hos Försäkringskassan — det räddar dagarna men ger ingen extra ledighet, och överföringen sker hos Försäkringskassan, inte i appen. Håll saldona här uppdaterade efter hand, så att prognosen bygger på vad ni faktiskt tog ut.', + 'tip.age4PoolingLossExact.title': 'Dagar ni ändå kan rädda genom att föra över', 'tip.age4PoolingLossHedged.body': - '{surplus_parent} har {excess_days} fler dagar kvar än vad som får sparas per förälder vid 4-årsdagen ({fourth_birthday}). Bäst är att ta ut dagarna som ledighet innan dess, det ger faktisk tid hemma. Om ni båda har dagar kvar kan ni annars överföra dagar mellan er hos Försäkringskassan — det räddar dagarna men ger ingen extra ledighet, och överföringen sker hos Försäkringskassan, inte i appen.', - 'tip.age4PoolingLossHedged.title': 'Ojämnt sparande kan kosta familjen dagar', + 'Planer ändras, och det här är bara en prognos utifrån kalendern som den ser ut i dag. Om den håller hamnar {surplus_parent} med {excess_days} fler dagar kvar än vad som får sparas per förälder vid 4-årsdagen ({fourth_birthday}). Bäst är att ta ut dagarna som ledighet innan dess, det ger faktisk tid hemma. Om ni båda har dagar kvar kan ni annars föra över dagar mellan er hos Försäkringskassan — det räddar dagarna men ger ingen extra ledighet, och överföringen sker hos Försäkringskassan, inte i appen. Håll saldona här uppdaterade efter hand, så att prognosen bygger på vad ni faktiskt tog ut.', + 'tip.age4PoolingLossHedged.title': 'Dagar ni kanske kan rädda genom att föra över', 'tip.aria': 'Tips: {title}', 'tip.balanceBetweenParents.body': '{leading_parent} har tagit ut {gap_days} fler dagar än {lagging_parent}. Fundera på att jämna ut fördelningen.', 'tip.balanceBetweenParents.title': 'Obalans mellan föräldrarna', @@ -13,7 +13,7 @@ export const tips = { 'tip.lagstaNivaCoverage.title': 'Täck upp med lägstanivå', 'tip.longBreakParentSwap.body': '{parent_name} täcker mindre än hälften av "{break_name}" ({start_date}–{end_date}). Låt {other_parent_name} ta en del av perioden.', 'tip.longBreakParentSwap.title': 'Överväg att växla av varandra', - 'tip.preserveForAge4.body': '{parent_name} riskerar att förlora {expiring_days} dagar när barnet fyller 4 år ({fourth_birthday}). Endast 96 dagar kan sparas efter det.', + 'tip.preserveForAge4.body': '{parent_name} riskerar att förlora {expiring_days} dagar när barnet fyller 4 år ({fourth_birthday}). Endast {parent_cap} dagar kan sparas per förälder efter det.', 'tip.preserveForAge4.title': 'Dagar riskerar att gå förlorade vid 4 års ålder', 'tip.reserveForPreschoolPlanningDays.body': '{parent_name} har {days_left} dagar kvar, vilket knappt räcker för förskolans {buffer_days} planeringsdagar. Reservera dagar i god tid.', 'tip.reserveForPreschoolPlanningDays.title': 'Reservera dagar för förskolans planeringsdagar', diff --git a/frontend/src/pages/Dashboard.test.tsx b/frontend/src/pages/Dashboard.test.tsx index af2fe2b..ee9ad70 100644 --- a/frontend/src/pages/Dashboard.test.tsx +++ b/frontend/src/pages/Dashboard.test.tsx @@ -275,6 +275,44 @@ describe('Dashboard', () => { renderWithRouter(