320 lines
8.8 KiB
Go
320 lines
8.8 KiB
Go
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)
|
||
if notify && a.notifier != nil {
|
||
_ = a.notifier.SendNotification(notifications.NotificationOptions{
|
||
ID: fmt.Sprintf("cc-%s-%d", kind, m.ID),
|
||
Title: title,
|
||
Body: body,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ---------- 到期巡检 ----------
|
||
|
||
// 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
|
||
}
|