134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package dyzurbot
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"time"
|
||
|
||
"google.golang.org/api/option"
|
||
"google.golang.org/api/sheets/v4"
|
||
)
|
||
|
||
// Column indices in the "Dyżury" sheet (see the header in row 0).
|
||
const (
|
||
colWeek = 0 // "Tydzień zaczynający się od" — week start date (serial)
|
||
colPerson1 = 1 // "Osoba 1"
|
||
colPerson2 = 2 // "Osoba 2"
|
||
colName = 4 // "Lista osób"
|
||
colCount = 5 // "Liczba dyżurów"
|
||
)
|
||
|
||
// sheetEpoch is the date corresponding to serial number 0 in Google Sheets.
|
||
var sheetEpoch = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
|
||
|
||
// DutyWeek is a single week of the duty rota (columns A–C).
|
||
type DutyWeek struct {
|
||
WeekStart time.Time // Monday that starts the week
|
||
Person1 string // Osoba 1 — "" if unassigned
|
||
Person2 string // Osoba 2 — "" if unassigned
|
||
}
|
||
|
||
// Person is a single entry from the people list (columns E–F).
|
||
type Person struct {
|
||
Name string // Lista osób
|
||
DutyCount int // Liczba dyżurów
|
||
}
|
||
|
||
// Rota is the parsed sheet contents: the week schedule and the people list.
|
||
type Rota struct {
|
||
Weeks []DutyWeek
|
||
People []Person
|
||
}
|
||
|
||
// ReadSheet reads the given range from the sheet and returns its rows.
|
||
// spreadsheetID is the ID from the sheet URL, readRange e.g. "Dyżury!A1:C30".
|
||
// The API key is read from GOOGLE_API_KEY (loaded into the environment by
|
||
// LoadConfig, which calls godotenv.Load at startup).
|
||
func ReadSheet(ctx context.Context, spreadsheetID, readRange string) ([][]interface{}, error) {
|
||
apiKey := os.Getenv("GOOGLE_API_KEY")
|
||
if apiKey == "" {
|
||
return nil, fmt.Errorf("missing GOOGLE_API_KEY in environment/.env")
|
||
}
|
||
|
||
srv, err := sheets.NewService(ctx, option.WithAPIKey(apiKey))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("init sheets service: %w", err)
|
||
}
|
||
|
||
resp, err := srv.Spreadsheets.Values.
|
||
Get(spreadsheetID, readRange).
|
||
ValueRenderOption("UNFORMATTED_VALUE"). // raw values, without cell formatting
|
||
Context(ctx).
|
||
Do()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("get values %q: %w", readRange, err)
|
||
}
|
||
|
||
return resp.Values, nil
|
||
}
|
||
|
||
// ParseRota interprets the raw rows from ReadSheet as two independent tables:
|
||
// the duty schedule (columns A–C) and the people list (columns E–F).
|
||
// The header row is skipped; malformed cells are ignored, not reported.
|
||
func ParseRota(rows [][]interface{}) Rota {
|
||
var rota Rota
|
||
|
||
for i, row := range rows {
|
||
if i == 0 {
|
||
continue // header
|
||
}
|
||
|
||
// Schedule: a row belongs to the weeks table if column A holds a
|
||
// date serial number.
|
||
if serial, ok := cellFloat(row, colWeek); ok {
|
||
rota.Weeks = append(rota.Weeks, DutyWeek{
|
||
WeekStart: serialToDate(serial),
|
||
Person1: cellString(row, colPerson1),
|
||
Person2: cellString(row, colPerson2),
|
||
})
|
||
}
|
||
|
||
// People list: add only non-empty entries (empty roster slots are
|
||
// skipped).
|
||
if name := cellString(row, colName); name != "" {
|
||
count, _ := cellFloat(row, colCount)
|
||
rota.People = append(rota.People, Person{
|
||
Name: name,
|
||
DutyCount: int(count),
|
||
})
|
||
}
|
||
}
|
||
|
||
return rota
|
||
}
|
||
|
||
// serialToDate converts a Google Sheets serial number to a date.
|
||
func serialToDate(serial float64) time.Time {
|
||
return sheetEpoch.AddDate(0, 0, int(serial))
|
||
}
|
||
|
||
// cellString returns the cell value as text, accounting for trailing empty
|
||
// cells that the API truncates. A missing cell yields "".
|
||
func cellString(row []interface{}, i int) string {
|
||
if i >= len(row) {
|
||
return ""
|
||
}
|
||
if s, ok := row[i].(string); ok {
|
||
return s
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// cellFloat returns the numeric value of a cell. ok=false when the cell is
|
||
// missing or not a number (e.g. an empty string).
|
||
func cellFloat(row []interface{}, i int) (float64, bool) {
|
||
if i >= len(row) {
|
||
return 0, false
|
||
}
|
||
if f, ok := row[i].(float64); ok {
|
||
return f, true
|
||
}
|
||
return 0, false
|
||
}
|