diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a79caf..d8b3928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ How to use this file: ## [Unreleased] +### Added + +- A person with payslips of several years can see one year at a time, or + all: a choice of years under their name narrows the whole report, its + months, charts and totals, and keeps the tab shown. A month is still + compared with the one before it in the year before. + ## [0.9.0] - 2026-10-22 ### Added diff --git a/cmd/cedolo/main.go b/cmd/cedolo/main.go index e42d4ea..c7f6bb8 100644 --- a/cmd/cedolo/main.go +++ b/cmd/cedolo/main.go @@ -239,6 +239,7 @@ func runSynth(args []string) error { password := fs.String("password", "", "protect the PDF with this password") rotate := fs.Int("rotate", 0, "turn the page by 90, 180 or 270 degrees") month := fs.Int("month", 10, "month of the payslip, 1 to 12") + year := fs.Int("year", 2026, "year of the payslip") person := fs.String("person", "", "worker's name, surname first, printed with an invented codice fiscale (classic style)") company := fs.String("employer", "", "employer's name, printed with an invented VAT number (classic style)") if err := fs.Parse(args); err != nil { @@ -258,6 +259,7 @@ func runSynth(args []string) error { opts := synth.Options{Password: *password, Rotate: *rotate} p := synth.ExampleFor(style) p.Month = time.Month(*month) + p.Year = *year if *person != "" { p.Employee, p.EmployeeCode = strings.ToUpper(*person), synth.CodiceFiscale(*person) } diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 04b14d7..ae02257 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -210,6 +210,8 @@ var catalog = map[Lang]map[string]string{ "report.aside": "Set aside for you", "report.aside.withheld": "Of this, %s is withheld from your pay: you also find it among the other deductions.", "report.people": "Whose payslips", + "report.years.all": "All", + "report.years.pick": "Year", "report.person.unknown": "Without fiscal code", "report.info": "Other information", "report.info.text": "They do not change this month's pay: amounts paid by the employer, or values for statistics.", @@ -409,6 +411,8 @@ var catalog = map[Lang]map[string]string{ "report.aside": "Messo da parte per te", "report.aside.withheld": "Di questi, %s sono trattenuti dalla tua busta: li trovi anche tra le altre trattenute.", "report.people": "Di chi sono i cedolini", + "report.years.all": "Tutti", + "report.years.pick": "Anno", "report.person.unknown": "Senza codice fiscale", "report.info": "Altre informazioni", "report.info.text": "Non cambiano la paga del mese: importi a carico dell'azienda o dati statistici.", diff --git a/internal/report/report.go b/internal/report/report.go index c0a3aab..fa6e838 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -123,7 +123,10 @@ type Page struct { // %s for the month, filled in by the page. OpenMonth string People []Data - Version string + // Views are every report the page can show: each person's, and each of + // their years when there are several. + Views []Data + Version string } // Data is everything the page shows of one person; BuildPage builds it @@ -135,6 +138,11 @@ type Data struct { // several; Person names them at the top of their report when there is // one. Who, Person string + // Index is the person's place among People; Year the year shown, 0 for + // all, and Choices the years to choose from, when there are several. + Index int + Year int + Choices []Choice // OpenMonth is the label of the button under a touched column, with a // %s for the month, filled in by the page. OpenMonth string @@ -193,8 +201,7 @@ func BuildPage(l i18n.Lang, slips []payslip.Payslip, version string) Page { insights.Sort(slips) pg := Page{Lang: string(l), T: texts(l), OpenMonth: i18n.Template(l, "report.chart.open"), Version: version} groups := people(slips) - for _, g := range groups { - d := Build(l, g, version) + for n, g := range groups { who := g[0].Person for _, p := range g { if p.Person.ID != "" { @@ -202,18 +209,50 @@ func BuildPage(l i18n.Lang, slips []payslip.Payslip, version string) Page { break } } - d.Who = PersonLabel(l, who) - if len(groups) == 1 && who.ID != "" { - d.Person = d.Who + // The person's report, and one for each year when there are + // several: the same choice of years tops each of them. + var ys []int + for _, p := range g { + if p.Year != 0 && !slices.Contains(ys, p.Year) { + ys = append(ys, p.Year) + } + } + if len(ys) < 2 { + ys = nil + } + for _, y := range append([]int{0}, ys...) { + d := build(l, g, version, y) + d.Index = n + d.Who = PersonLabel(l, who) + if len(groups) == 1 && who.ID != "" { + d.Person = d.Who + } + if ys != nil { + d.Choices = append(d.Choices, Choice{Label: i18n.T(l, "report.years.all"), On: y == 0}) + for _, c := range ys { + d.Choices = append(d.Choices, Choice{Label: strconv.Itoa(c), Year: strconv.Itoa(c), On: c == y}) + } + } + if y == 0 { + pg.People = append(pg.People, d) + } + pg.Views = append(pg.Views, d) } - pg.People = append(pg.People, d) } if len(pg.People) == 0 { pg.People = []Data{Build(l, nil, version)} + pg.Views = pg.People } return pg } +// Choice is a button of the choice of years. +type Choice struct { + Label string + Year string // empty for all of them + On bool +} + // PersonLabel names a person: by the name read, or else by the masked // codice fiscale, or as one without it. func PersonLabel(l i18n.Lang, p payslip.Party) string { @@ -315,19 +354,35 @@ func shortEmployer(e payslip.Party) string { // Build computes the page content. The slice is sorted in place. func Build(l i18n.Lang, slips []payslip.Payslip, version string) Data { + return build(l, slips, version, 0) +} + +// build computes the page content of the payslips of one year, or of all +// of them for year 0. A month is still compared with the one before it in +// another year. The slice is sorted in place. +func build(l i18n.Lang, slips []payslip.Payslip, version string, year int) Data { periods := insights.Compare(l, slips) // sorts slips - d := Data{Lang: string(l), T: texts(l), OpenMonth: i18n.Template(l, "report.chart.open"), Version: version} - if len(slips) == 0 { + d := Data{Lang: string(l), T: texts(l), OpenMonth: i18n.Template(l, "report.chart.open"), Version: version, Year: year} + var kept []int + var shown []payslip.Payslip + for i, p := range slips { + if year == 0 || p.Year == year { + kept = append(kept, i) + shown = append(shown, p) + } + } + if len(kept) == 0 { return d } - if len(slips) == 1 { - d.Range = i18n.T(l, "report.range.one", periods[0].Label) + if len(kept) == 1 { + d.Range = i18n.T(l, "report.range.one", periods[kept[0]].Label) } else { - d.Range = i18n.T(l, "report.range", len(slips), periods[0].Label, periods[len(periods)-1].Label) + d.Range = i18n.T(l, "report.range", len(kept), periods[kept[0]].Label, periods[kept[len(kept)-1]].Label) } names := employers(slips) - for i, p := range slips { + for k, i := range kept { + p := slips[i] st := status(p) d.Rows = append(d.Rows, Row{ Period: periods[i].Label, @@ -343,7 +398,7 @@ func Build(l i18n.Lang, slips []payslip.Payslip, version string) Data { m := month(l, p, periods[i]) if name, ok := names[p.Employer.ID]; ok { m.Label += " · " + name - if sameShort(slips, i) { + if sameShort(shown, k) { m.Short += " · " + name } } @@ -378,19 +433,19 @@ func Build(l i18n.Lang, slips []payslip.Payslip, version string) Data { } } // The latest payslip opens first, a thirteenth or fourteenth month too. - d.Selected = len(slips) - 1 - - if len(slips) > 1 { - d.Trend = template.HTML(trend(l, slips)) - d.Variable = template.HTML(variablePay(l, slips)) - d.Years = years(l, slips) - d.TFR, d.TFRTotal, d.TFRSpan = tfrChart(l, slips) - d.Fund, d.FundTotal, d.FundSpan = fundChart(l, slips) + d.Selected = len(shown) - 1 + + if len(shown) > 1 { + d.Trend = template.HTML(trend(l, shown)) + d.Variable = template.HTML(variablePay(l, shown)) + d.Years = years(l, shown) + d.TFR, d.TFRTotal, d.TFRSpan = tfrChart(l, shown) + d.Fund, d.FundTotal, d.FundSpan = fundChart(l, shown) if d.Fund != "" { - d.FundYears = fundYears(l, slips) + d.FundYears = fundYears(l, shown) } } - d.LeaveCharts = leaveCharts(l, slips) + d.LeaveCharts = leaveCharts(l, shown) d.Time = d.Years != nil || d.Trend != "" || d.Variable != "" d.Accrued = len(d.LeaveCharts) > 0 || d.TFR != "" || d.Fund != "" d.Tabs = d.Time || d.Accrued @@ -401,7 +456,7 @@ func Build(l i18n.Lang, slips []payslip.Payslip, version string) Data { } } - d.Contract = contract(l, slips) + d.Contract = contract(l, shown) for _, term := range i18n.Terms { d.Terms = append(d.Terms, Term{Label: i18n.T(l, "term."+term), Text: i18n.T(l, "term."+term+".text")}) } diff --git a/internal/report/report.html.tmpl b/internal/report/report.html.tmpl index 95604ed..9f2ec51 100644 --- a/internal/report/report.html.tmpl +++ b/internal/report/report.html.tmpl @@ -113,6 +113,9 @@ p { margin: 0 0 12px; } .people button { font: inherit; font-size: 15px; font-weight: 600; padding: 8px 14px; border-radius: 999px; border: 1px solid var(--grid); background: var(--raised); color: var(--ink); cursor: pointer; } .people button[aria-pressed="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); } .who .person { font-size: 17px; font-weight: 600; margin: 4px 0 0; } +.yearpick { display: flex; gap: 2px; margin: 10px 0 2px; padding: 3px; width: fit-content; max-width: 100%; overflow-x: auto; scrollbar-width: none; border-radius: 10px; background: var(--grid); } +.yearpick button { flex: none; font: inherit; font-size: 14px; padding: 5px 12px; border: 0; border-radius: 8px; background: none; color: var(--ink-2); cursor: pointer; font-variant-numeric: tabular-nums; } +.yearpick button[aria-pressed="true"] { background: var(--raised); color: var(--ink); font-weight: 600; box-shadow: 0 1px 2px rgb(0 0 0 / .12); } .month { margin-top: 24px; } .when { color: var(--ink-2); margin: 0; } .when .status { font-size: 14px; margin-left: 8px; } @@ -230,7 +233,7 @@ footer { margin-top: 32px; font-size: 13px; color: var(--muted); } .chart.narrow { display: block; } } @media print { - .tooltip, .picker, .hint, .tabs, .steps, .people { display: none; } + .tooltip, .picker, .hint, .tabs, .steps, .people, .yearpick { display: none; } .panel[hidden] { display: block !important; } .tabs ~ .panel h2.group { display: block; } .month[hidden] { display: block !important; break-before: page; } @@ -246,7 +249,7 @@ footer { margin-top: 32px; font-size: 13px; color: var(--muted); } {{end}} {{end}}
{{template "person" index .People 0}}
-{{if gt (len .People) 1}}{{range $i, $p := .People}} +{{if gt (len .Views) 1}}{{range .Views}} {{end}}{{end}}