- {sections.map((section) => (
-
-
- {t(section.labelKey)}
-
-
-
- ))}
+
+
+
+
+ | {t('col_report')} |
+ {t('col_description')} |
+ {t('col_last_opened')} |
+
+
+
+ {sections.map((section) => (
+
+ ))}
+
+
)
}
-function EntityBadge({ item }: { item: ReportDescriptor }) {
+function SectionRows({
+ label,
+ items,
+ lastOpenedLabel,
+ onOpen,
+}: {
+ label: string
+ items: ReportDescriptor[]
+ lastOpenedLabel: (slug: string) => string
+ onOpen: (slug: string) => void
+}) {
+ const t = useTranslations('reports')
+ return (
+ <>
+
+ |
+ {label}
+ |
+
+ {items.map((item) => (
+
onOpen(item.slug)}
+ >
+ |
+
+ {t(item.labelKey)}
+
+ {item.params === 'calendar' && (
+
+ {t('calendar_badge')}
+
+ )}
+
+ |
+ {t(item.descKey)} |
+
+ {lastOpenedLabel(item.slug)}
+ |
+
+ ))}
+ >
+ )
+}
+
+function EntityMark({ item }: { item: ReportDescriptor }) {
if (item.entityType === 'enskild_firma')
return
EF
if (item.entityType === 'aktiebolag')
diff --git a/components/reports/useRecentReports.ts b/components/reports/useRecentReports.ts
index 3ca1c19b..0f383f5e 100644
--- a/components/reports/useRecentReports.ts
+++ b/components/reports/useRecentReports.ts
@@ -3,33 +3,43 @@
import { useCallback, useEffect, useState } from 'react'
/**
- * Tracks the last few report slugs the user opened, per company, in
- * localStorage. Mirrors the `Accounted:
:` convention used by
- * FiscalYearSelector (STORAGE_KEY_PREFIX). Powers the "Senast öppnade" shelf
- * so returning users skip the library hop.
+ * Tracks the last reports the user opened, per company, in localStorage.
+ * Mirrors the `Accounted::` convention used by
+ * FiscalYearSelector (STORAGE_KEY_PREFIX). Powers the "Senast öppnad" column
+ * in the report catalog table.
+ *
+ * Storage format: Array<{ s: slug, at: epoch-ms }>. Legacy entries were plain
+ * slug strings; those parse without a timestamp and simply show no date.
*/
const STORAGE_KEY_PREFIX = 'Accounted:report-recents:'
-const MAX_RECENTS = 4
+const MAX_RECENTS = 12
+
+type StoredRecent = string | { s: string; at: number }
export function useRecentReports(companyId: string | null | undefined) {
- const [recents, setRecents] = useState([])
+ const [openedAt, setOpenedAt] = useState>({})
useEffect(() => {
let cancelled = false
// Deferred to a microtask so the read isn't a synchronous setState in the
// effect body (and so the first server/client render agree on an empty
- // shelf, avoiding a hydration mismatch).
+ // list, avoiding a hydration mismatch).
Promise.resolve().then(() => {
if (cancelled) return
if (!companyId) {
- setRecents([])
+ setOpenedAt({})
return
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY_PREFIX + companyId)
- setRecents(raw ? (JSON.parse(raw) as string[]) : [])
+ const parsed = raw ? (JSON.parse(raw) as StoredRecent[]) : []
+ const map: Record = {}
+ for (const entry of parsed) {
+ if (typeof entry === 'object' && entry && entry.s) map[entry.s] = entry.at
+ }
+ setOpenedAt(map)
} catch {
- setRecents([])
+ setOpenedAt({})
}
})
return () => {
@@ -40,10 +50,17 @@ export function useRecentReports(companyId: string | null | undefined) {
const pushRecent = useCallback(
(slug: string) => {
if (!companyId) return
- setRecents((prev) => {
- const next = [slug, ...prev.filter((s) => s !== slug)].slice(0, MAX_RECENTS)
+ setOpenedAt((prev) => {
+ const next = { ...prev, [slug]: Date.now() }
try {
- window.localStorage.setItem(STORAGE_KEY_PREFIX + companyId, JSON.stringify(next))
+ const stored = Object.entries(next)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, MAX_RECENTS)
+ .map(([s, at]) => ({ s, at }))
+ window.localStorage.setItem(
+ STORAGE_KEY_PREFIX + companyId,
+ JSON.stringify(stored),
+ )
} catch {
/* localStorage unavailable: keep in-memory only */
}
@@ -53,5 +70,5 @@ export function useRecentReports(companyId: string | null | undefined) {
[companyId],
)
- return { recents, pushRecent }
+ return { openedAt, pushRecent }
}
diff --git a/messages/en.json b/messages/en.json
index 4f989385..ee3af37c 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -5229,7 +5229,14 @@
"date_range_preset_this_quarter": "This quarter",
"date_range_preset_custom": "Custom",
"date_range_from": "From",
- "date_range_to": "To"
+ "date_range_to": "To",
+ "col_report": "Report",
+ "col_description": "Description",
+ "col_last_opened": "Last opened",
+ "opened_today": "today",
+ "opened_yesterday": "yesterday",
+ "help_text": "Every report opens for the selected fiscal year and can be narrowed to any period inside the report. Last opened remembers what you use the most.",
+ "catalog_footnote": "Every report opens for any period and can be downloaded as a PDF. The NE attachment replaces INK2 for sole traders."
},
"salary": {
"title": "Payroll",
diff --git a/messages/sv.json b/messages/sv.json
index add4e410..462eee90 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -5229,7 +5229,14 @@
"date_range_preset_this_quarter": "Detta kvartal",
"date_range_preset_custom": "Anpassat",
"date_range_from": "Från",
- "date_range_to": "Till"
+ "date_range_to": "Till",
+ "col_report": "Rapport",
+ "col_description": "Beskrivning",
+ "col_last_opened": "Senast öppnad",
+ "opened_today": "idag",
+ "opened_yesterday": "igår",
+ "help_text": "Alla rapporter öppnas för det valda räkenskapsåret och kan avgränsas till valfri period inne i rapporten. Senast öppnad kommer ihåg vad du använder mest.",
+ "catalog_footnote": "Alla rapporter öppnas för valfri period och kan laddas ner som PDF. NE-bilagan visas i stället för INK2 för enskild firma."
},
"salary": {
"title": "Löner",