diff --git a/DECISIONS.md b/DECISIONS.md index 6b3d826f..9416661c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1089,3 +1089,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-19] Trial-expired visibility: the 2026-07-11 "no trial-expired nag" call is narrowed at the founder's direction (user-reported discoverability failure 2026-08-18): one on-entry dialog with persisted acknowledgement (user_preferences.ui_state.trial_expired_ack, per user AND company, never localStorage) plus a non-dismissable quiet chrome link (SubscriptionTouchpoint replaces the vanishing countdown pill, stays visible when the rail is collapsed, and adds the first mobile touchpoint). Free tier stays a legitimate resting state: no recurring nag, muted chrome tone, no ochre. getCompanyEntitlements also reads company_subscriptions.status inside its existing Promise.all (zero extra round-trips) so churned payers get "abonnemang" copy instead of "provperiod". Hem AttnLine variant skipped: the page already carries the otherAccountHint AttnLine and design.md allows max one per page. [2026-08-19] Removed PeriodiseringAutoDetectToggle (settings > Automatik) instead of wiring it: the localStorage key it wrote (periodisering_autodetect_enabled) had no reader anywhere, so it advertised "automatisk periodiseringsdetektering" while changing nothing; auto-detect is already best-effort and review-gated in the wizard, so the row is now a plain link to the periodisering wizard. Deleting the key is safe: it was write-only. [2026-08-19] Periodisering auto-detect materiality floor uses entity_type as a K1 proxy: no stored flag distinguishes förenklat årsbokslut (K1, BFNAR 2006:1) from full årsbokslut (BFNAR 2017:3) for enskild firma, so every EF gets K1 wording and every AB gets K2, always advisory ("behöver normalt inte"), never prohibitive. Suggestions under 5 000 kr are tagged low-confidence (unticked) rather than dropped because the relief is a MAY, not a MUST; personnel-cost lines (7xxx) are exempt from the floor since K1/K2 require personnel costs to always be accrued. +[2026-08-19] Hem build-assistant hero downgraded to the quiet-sentence pattern (AgentPromo, matches SkatteverketPromoCard; founder direction 2026-08-18 'redesign first, maybe remove later'): dismissal is per-company localStorage (erp_agent_promo_dismissed:) like the SKV promo, gate and hasAi/billing routing unchanged. diff --git a/components/dashboard/AgentPromo.tsx b/components/dashboard/AgentPromo.tsx new file mode 100644 index 00000000..cd2e037b --- /dev/null +++ b/components/dashboard/AgentPromo.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useCallback, useSyncExternalStore } from 'react' +import Link from 'next/link' +import { useTranslations } from 'next-intl' +import { useCapability } from '@/contexts/CompanyContext' +import { CAPABILITY } from '@/lib/entitlements/keys' + +const dismissKey = (companyId: string) => `erp_agent_promo_dismissed:${companyId}` +// storage events only fire in OTHER tabs; this custom event covers the +// same-tab dismissal so useSyncExternalStore re-reads localStorage. +const DISMISS_EVENT = 'erp-agent-promo-dismissed' + +function subscribeToDismissal(onStoreChange: () => void) { + window.addEventListener('storage', onStoreChange) + window.addEventListener(DISMISS_EVENT, onStoreChange) + return () => { + window.removeEventListener('storage', onStoreChange) + window.removeEventListener(DISMISS_EVENT, onStoreChange) + } +} + +interface AgentPromoProps { + companyId: string +} + +/** + * Build-assistant nudge on Hem, shown until the company has a verified + * agent_profile (the caller gates on that plus the first-run checklist + * being dismissed or completed, since the checklist already carries the + * assistant as its last step). + * Shape: one quiet sentence with the action link at the end and the same + * "Dölj" text control the Skatteverket promo uses, not a boxed card. It is + * an optional offer, not an exception, so it stays muted (convention 6/12). + * Non-payers keep seeing it (conversion surface) but it routes to billing + * instead of a build flow that would 403. + */ +export function AgentPromo({ companyId }: AgentPromoProps) { + const t = useTranslations('dashboard') + const hasAi = useCapability(CAPABILITY.ai) + + // Server snapshot says dismissed: the promo appears only after hydration, + // when localStorage is readable, so server and client never disagree. + const dismissed = useSyncExternalStore( + subscribeToDismissal, + () => localStorage.getItem(dismissKey(companyId)) === 'true', + () => true + ) + + const dismiss = useCallback(() => { + localStorage.setItem(dismissKey(companyId), 'true') + window.dispatchEvent(new Event(DISMISS_EVENT)) + }, [companyId]) + + if (dismissed) return null + + return ( +
+

+ {t(hasAi ? 'agent_promo_description' : 'agent_promo_description_upgrade')}{' '} + {/* py-3/-my-3: a 44px tap target on a link that still sits inline in + the sentence; the negative margin keeps the line box at 20px so + nothing shifts. */} + + {t(hasAi ? 'agent_promo_cta' : 'agent_promo_cta_upgrade')} + +

+ +
+ ) +} diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx index 6aca4670..a7c5d77a 100644 --- a/components/dashboard/DashboardContent.tsx +++ b/components/dashboard/DashboardContent.tsx @@ -1,21 +1,17 @@ 'use client' import { useState } from 'react' -import Link from 'next/link' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { createClient } from '@/lib/supabase/client' import { AttnLine } from '@/components/ui/attn-line' -import { Card, CardContent } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { useCapability, useCompany } from '@/contexts/CompanyContext' -import { CAPABILITY } from '@/lib/entitlements/keys' +import { useCompany } from '@/contexts/CompanyContext' import NewUserChecklist from '@/components/onboarding/NewUserChecklist' import AttGoraSection from '@/components/dashboard/AttGoraSection' import ResumePane from '@/components/dashboard/ResumePane' import BackupHealthBanner from '@/components/dashboard/BackupHealthBanner' import { SkatteverketPromoCard } from '@/components/dashboard/SkatteverketPromoCard' -import { ArrowRight } from 'lucide-react' +import { AgentPromo } from '@/components/dashboard/AgentPromo' import type { InitialSetupState, OnboardingProgress } from '@/types' import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types' import type { ResumeItem } from '@/lib/worklist/resume' @@ -85,7 +81,6 @@ export default function DashboardContent({ sieSweep = null, }: DashboardContentProps) { const t = useTranslations('dashboard') - const hasAi = useCapability(CAPABILITY.ai) const { company } = useCompany() const router = useRouter() @@ -144,37 +139,13 @@ export default function DashboardContent({ sieSweep={sieSweep} /> - {/* Build-assistant hero: shown only until the company has a verified + {/* Build-assistant nudge: shown only until the company has a verified agent_profile, so existing/migrated users get a clear prompt instead of a full-screen onboarding takeover. While the stepped first-run checklist is visible it already carries the assistant as its last - step, so the hero waits until that block is dismissed or completed. */} + step, so the promo waits until that block is dismissed or completed. */} {!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && ( -
- {/* Non-payers keep seeing the hero (conversion surface) but it - routes to billing instead of a build flow that would 403. */} - - - -
-
-

Bygg din bokföringsassistent

- Beta -
-

- {hasAi - ? 'Några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.' - : 'Ingår i abonnemanget: en assistent som föreslår bokföring åt dig.'} -

-
-
- {hasAi ? 'Kom igång' : 'Uppgradera'} - -
-
-
- -
+ )} {/* The two panes (concept hem-grid). When nothing is in progress the diff --git a/messages/en.json b/messages/en.json index 852ad42e..4a4c7122 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6395,6 +6395,11 @@ "skv_promo_description": "See your tax account and file VAT and employer declarations directly from here. Connect with BankID in a couple of minutes.", "skv_promo_cta": "Connect", "skv_promo_dismiss": "Dismiss suggestion", + "agent_promo_description": "Build your bookkeeping assistant (beta): a few questions about your business calibrate an assistant that suggests bookkeeping for you.", + "agent_promo_description_upgrade": "The bookkeeping assistant (beta) is included in the subscription: an assistant that suggests bookkeeping for you.", + "agent_promo_cta": "Get started", + "agent_promo_cta_upgrade": "Upgrade", + "agent_promo_dismiss": "Dismiss suggestion", "result": "Net result", "this_year_short": "this year", "this_month": "this month", diff --git a/messages/sv.json b/messages/sv.json index b73027e9..a7c0034f 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -6395,6 +6395,11 @@ "skv_promo_description": "Se skattekontot och lämna moms- och arbetsgivardeklarationer direkt härifrån. Anslut med BankID på ett par minuter.", "skv_promo_cta": "Anslut", "skv_promo_dismiss": "Dölj förslaget", + "agent_promo_description": "Bygg din bokföringsassistent (beta): några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.", + "agent_promo_description_upgrade": "Bokföringsassistenten (beta) ingår i abonnemanget: en assistent som föreslår bokföring åt dig.", + "agent_promo_cta": "Kom igång", + "agent_promo_cta_upgrade": "Uppgradera", + "agent_promo_dismiss": "Dölj förslaget", "result": "Resultat", "this_year_short": "i år", "this_month": "denna månad",