Files
code-utils/workbench.go

331 lines
9.1 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
import (
"fmt"
"strconv"
"time"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
// ---------- Todo 绑定 ----------
func (a *App) ListTodos(status string, projectID int64) ([]Todo, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListTodos(status, projectID)
}
func (a *App) SaveTodo(t Todo) (Todo, error) {
if e := a.ready(); e != nil {
return t, e
}
return a.store.SaveTodo(t)
}
func (a *App) SetTodoStatus(id int64, status string) error {
if e := a.ready(); e != nil {
return e
}
return a.store.SetTodoStatus(id, status)
}
func (a *App) DeleteTodo(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteTodo(id)
}
// ---------- 工单绑定 ----------
func (a *App) ListTickets(status string, projectID int64) ([]Ticket, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListTickets(status, projectID)
}
func (a *App) SaveTicket(t Ticket) (Ticket, error) {
if e := a.ready(); e != nil {
return t, e
}
return a.store.SaveTicket(t)
}
func (a *App) SetTicketStatus(id int64, status string) error {
if e := a.ready(); e != nil {
return e
}
return a.store.SetTicketStatus(id, status)
}
func (a *App) DeleteTicket(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteTicket(id)
}
// ---------- 记事本与收藏 ----------
func (a *App) GetNote() (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.GetNote()
}
func (a *App) SaveNote(content string) (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.SaveNote(content)
}
func (a *App) ListNotes(limit int) ([]Note, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListNotes(limit)
}
// SaveNoteByID 保存指定笔记id=0 新建。
func (a *App) SaveNoteByID(id int64, content string) (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.SaveNoteByID(id, content)
}
func (a *App) DeleteNote(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteNote(id)
}
func (a *App) ListFavorites() ([]int64, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListFavorites()
}
func (a *App) ToggleFavorite(projectID int64) (bool, error) {
if e := a.ready(); e != nil {
return false, e
}
return a.store.ToggleFavorite(projectID)
}
// ---------- 消息中心 ----------
func (a *App) ListMessages(limit int64) ([]Message, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListMessages(limit)
}
func (a *App) UnreadMessageCount() (int64, error) {
if e := a.ready(); e != nil {
return 0, e
}
return a.store.UnreadMessageCount(), nil
}
func (a *App) MarkMessageRead(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.MarkMessageRead(id)
}
func (a *App) MarkAllMessagesRead() error {
if e := a.ready(); e != nil {
return e
}
return a.store.MarkAllMessagesRead()
}
func (a *App) ClearMessages() error {
if e := a.ready(); e != nil {
return e
}
return a.store.ClearMessages()
}
// pushMessage 写入消息中心、广播给前端,并可选发送系统通知。
func (a *App) pushMessage(kind, title, body, sourceType string, sourceID int64, notify bool) {
if a.store == nil {
return
}
m, e := a.store.AddMessage(kind, title, body, sourceType, sourceID)
if e != nil {
return
}
a.emit("message:new", m)
2026-08-14 17:54:06 +08:00
a.RefreshTrayStatus()
2026-08-14 07:52:01 +08:00
if notify && a.notifier != nil {
2026-08-14 17:54:06 +08:00
path := "/messages"
switch sourceType {
case "todo":
path = "/todos"
case "ticket":
path = "/tickets"
case "team":
path = "/team/tasks"
}
2026-08-14 07:52:01 +08:00
_ = a.notifier.SendNotification(notifications.NotificationOptions{
ID: fmt.Sprintf("cc-%s-%d", kind, m.ID),
Title: title,
Body: body,
2026-08-14 17:54:06 +08:00
Data: map[string]interface{}{"path": path, "kind": kind, "sourceType": sourceType, "sourceId": sourceID},
2026-08-14 07:52:01 +08:00
})
}
}
// ---------- 到期巡检 ----------
// parseDue 解析待办/工单的排期时间(本地时区),支持日期或日期+时间。
func parseDue(v string) (time.Time, bool) {
for _, layout := range []string{"2006-01-02T15:04", "2006-01-02 15:04", "2006-01-02"} {
if t, e := time.ParseInLocation(layout, v, time.Local); e == nil {
// 纯日期视为当天 23:59 截止
if layout == "2006-01-02" {
t = t.Add(23*time.Hour + 59*time.Minute)
}
return t, true
}
}
return time.Time{}, false
}
// runReminderLoop 每分钟巡检到期 todo/工单24 小时内到期与已逾期各提醒一次。
func (a *App) runReminderLoop() {
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-a.ctx.Done():
return
case <-t.C:
a.checkDueReminders()
}
}
}
const (
remindedSoon = 1
remindedOverdue = 2
)
// reminderTexts 返回按当前语言本地化的提醒文案:[todo 逾期, todo 将到期, 工单逾期, 工单将到期, "截止"前缀]。
func (a *App) reminderTexts() [5]string {
locale := "zh-CN"
if st, e := a.store.Settings(); e == nil {
locale = st.Locale
}
if locale == "en" {
return [5]string{"Todo overdue", "Todo due soon", "Ticket overdue", "Ticket due soon", "due"}
}
return [5]string{"待办已逾期", "待办即将到期", "工单已逾期", "工单即将到期", "截止"}
}
func (a *App) checkDueReminders() {
if a.store == nil || a.bootstrap.State != BootstrapReady {
return
}
txt := a.reminderTexts()
now := time.Now()
type row struct {
id int64
title string
due string
reminded int64
}
scan := func(query string) []row {
rows, e := a.store.db.Query(query)
if e != nil {
return nil
}
defer rows.Close()
out := []row{}
for rows.Next() {
var x row
if rows.Scan(&x.id, &x.title, &x.due, &x.reminded) == nil {
out = append(out, x)
}
}
return out
}
for _, x := range scan(`SELECT id,title,due_at,reminded FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`) {
due, ok := parseDue(x.due)
if !ok {
continue
}
if now.After(due) && x.reminded&remindedOverdue == 0 {
a.pushMessage("todo_due", txt[0], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true)
_, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id)
} else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 {
a.pushMessage("todo_due", txt[1], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true)
_, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id)
}
}
for _, x := range scan(`SELECT id,title,due_at,reminded FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`) {
due, ok := parseDue(x.due)
if !ok {
continue
}
if now.After(due) && x.reminded&remindedOverdue == 0 {
a.pushMessage("ticket_due", txt[2], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true)
_, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id)
} else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 {
a.pushMessage("ticket_due", txt[3], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true)
_, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id)
}
}
}
// ---------- 导航徽标 ----------
// NavBadges 左侧导航徽标数据(一次查询打包返回)。
type NavBadges struct {
Unread int `json:"unread"` // 消息中心:未读
TodosOpen int `json:"todosOpen"` // 待办事项:未完成
TicketsActive int `json:"ticketsActive"` // 需求工单:开放 + 进行中
TodayDue int `json:"todayDue"` // 今日任务:逾期 + 今日截止(未完成)
CalendarDot bool `json:"calendarDot"` // 排期日历:今天有到期事项
TeamAssigned int `json:"teamAssigned"` // 团队任务:指派给我未完成(同步时缓存)
}
func (a *App) GetNavBadges() (NavBadges, error) {
if e := a.ready(); e != nil {
return NavBadges{}, e
}
var b NavBadges
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM messages WHERE is_read=0`).Scan(&b.Unread)
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM todos WHERE deleted=0 AND status IN ('open','doing')`).Scan(&b.TodosOpen)
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM tickets WHERE deleted=0 AND status IN ('open','in_progress')`).Scan(&b.TicketsActive)
b.TodayDue = a.countDueToday()
b.CalendarDot = b.TodayDue > 0
if a.syncUserID() > 0 {
b.TeamAssigned, _ = strconv.Atoi(a.store.Meta("team_assigned_cache"))
}
return b, nil
}
// countDueToday 统计逾期与今日截止的未完成待办/工单。
func (a *App) countDueToday() int {
now := time.Now()
end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
n := 0
for _, q := range []string{
`SELECT due_at FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`,
`SELECT due_at FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`,
} {
rows, e := a.store.db.Query(q)
if e != nil {
continue
}
for rows.Next() {
var v string
if rows.Scan(&v) != nil {
continue
}
if due, ok := parseDue(v); ok && !due.After(end) {
n++
}
}
rows.Close()
}
return n
}