feat: Hijri calendar, virtual midnight crossover, overlap rejection, WAL mode
- Hijri (قمری) calendar added as 3rd calendar option with tabular conversion - Calendar type select menu (like accent) with gregorian/jalali/hijri - Calendar type applied to exports and reports - Midnight crossover: virtual splits at computation time (no synthetic DB events) - Handles both UTC and local timezone boundaries - Timezone changes handled naturally (recomputed on-the-fly) - Rate limiting per user ID instead of chat ID - WAL mode enabled for SQLite - Smart reply keyboard updates after every state change - Overlapping time edits rejected (must preserve in/out alternation) - Single event deletion warns if timeline needs repair - Day off no longer prevents clock in/out - Accent colors shown with descriptive names in select menu - Delete button label changed to DEL for visual distinction
This commit is contained in:
@@ -22,7 +22,7 @@ var themes = map[string]themeColors{
|
||||
"catppuccin": {headerFill: "B48DED", border: "D9D0E8", totalFill: "F0E8FC"},
|
||||
}
|
||||
|
||||
func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent string, year int, month time.Month) ([]byte, error) {
|
||||
func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, calendar string, year int, month time.Month) ([]byte, error) {
|
||||
theme, ok := themes[accent]
|
||||
if !ok {
|
||||
theme = themes["ocean"]
|
||||
@@ -76,7 +76,15 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent strin
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
||||
})
|
||||
|
||||
monthName := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).Format("January 2006")
|
||||
var monthName string
|
||||
switch calendar {
|
||||
case "jalali":
|
||||
monthName = fmt.Sprintf("%s %d", jalaliMonthNames[month-1], year)
|
||||
case "hijri":
|
||||
monthName = fmt.Sprintf("%s %d", hijriMonthNames[month-1], year)
|
||||
default:
|
||||
monthName = time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).Format("January 2006")
|
||||
}
|
||||
f.SetCellValue(sheet, "A1", fmt.Sprintf("Work Time Report - %s", monthName))
|
||||
f.MergeCell(sheet, "A1", "F1")
|
||||
f.SetCellStyle(sheet, "A1", "F1", titleStyle)
|
||||
@@ -120,15 +128,21 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent strin
|
||||
|
||||
for d := firstDay; !d.After(lastDay); d = d.AddDate(0, 0, 1) {
|
||||
dateStr := d.Format("2006-01-02")
|
||||
displayDate := dateStr
|
||||
if calendar == "jalali" {
|
||||
gy, gm, gd := parseGregorianDateKey(dateStr)
|
||||
jy, jm, jd := gregorianToJalali(gy, gm, gd)
|
||||
displayDate = fmt.Sprintf("%04d-%02d-%02d", jy, jm, jd)
|
||||
} else if calendar == "hijri" {
|
||||
gy, gm, gd := parseGregorianDateKey(dateStr)
|
||||
hy, hm, hd := gregorianToHijri(gy, gm, gd)
|
||||
displayDate = fmt.Sprintf("%04d-%02d-%02d", hy, hm, hd)
|
||||
}
|
||||
|
||||
day, exists := dayMap[dateStr]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if day.IsDayOff {
|
||||
continue
|
||||
}
|
||||
|
||||
events, err := store.EventsForDayByDayID(day.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -137,12 +151,14 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent strin
|
||||
continue
|
||||
}
|
||||
|
||||
totals := ComputeDailyTotals(events, day.MinBreakThreshold)
|
||||
dayStart := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, loc).Unix()
|
||||
dayEnd := time.Date(d.Year(), d.Month(), d.Day(), 23, 59, 59, 0, loc).Unix()
|
||||
totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
|
||||
totalWork += totals.TotalSeconds
|
||||
totalBreak += totals.BreakSeconds
|
||||
|
||||
cellDate, _ := excelize.CoordinatesToCellName(1, row)
|
||||
f.SetCellValue(sheet, cellDate, dateStr)
|
||||
f.SetCellValue(sheet, cellDate, displayDate)
|
||||
f.SetCellStyle(sheet, cellDate, cellDate, dataStyle)
|
||||
|
||||
if len(events) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user