175 lines
5.0 KiB
Go
175 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
|
|
"cnc-sales-backend/internal/auth"
|
|
"cnc-sales-backend/internal/models"
|
|
)
|
|
|
|
// todayPanel 今日待处理聚合:逾期任务 / 今日到期任务 / 跟进到期客户
|
|
func (s *Server) todayPanel(w http.ResponseWriter, r *http.Request) {
|
|
uid := auth.UserID(r)
|
|
today := todayStr()
|
|
|
|
overdue := []models.Task{}
|
|
rows, err := s.db.Query(`SELECT `+taskCols+` FROM tasks t
|
|
LEFT JOIN customers c ON c.id=t.customer_id
|
|
WHERE t.owner_id=? AND t.deleted_at IS NULL AND t.status!='done' AND t.due_date!='' AND t.due_date<?`,
|
|
uid, today)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
if t, ok := scanTask(rows); ok {
|
|
overdue = append(overdue, *t)
|
|
}
|
|
}
|
|
}
|
|
|
|
dueToday := []models.Task{}
|
|
rows, err = s.db.Query(`SELECT `+taskCols+` FROM tasks t
|
|
LEFT JOIN customers c ON c.id=t.customer_id
|
|
WHERE t.owner_id=? AND t.deleted_at IS NULL AND t.status!='done' AND t.due_date=?`,
|
|
uid, today)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
if t, ok := scanTask(rows); ok {
|
|
dueToday = append(dueToday, *t)
|
|
}
|
|
}
|
|
}
|
|
|
|
dueFollowUps := []models.Customer{}
|
|
rows, err = s.db.Query(`SELECT `+customerCols+` FROM customers
|
|
WHERE owner_id=? AND deleted_at IS NULL AND next_follow_date!='' AND next_follow_date<=?`,
|
|
uid, today)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
c := models.Customer{}
|
|
if err := rows.Scan(&c.ID, &c.Company, &c.Industry, &c.Address, &c.Level, &c.Stage,
|
|
&c.MachineModel, &c.Budget, &c.WorkpieceNeeds, &c.PainPoints,
|
|
&c.NextFollowDate, &c.Source, &c.Remark, &c.CreatedAt, &c.UpdatedAt); err == nil {
|
|
dueFollowUps = append(dueFollowUps, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"today": today,
|
|
"overdueTasks": overdue,
|
|
"dueTodayTasks": dueToday,
|
|
"dueFollowUps": dueFollowUps,
|
|
})
|
|
}
|
|
|
|
func scanTask(rows *sql.Rows) (*models.Task, bool) {
|
|
t := &models.Task{}
|
|
var customerID, repeat sql.NullString
|
|
if err := rows.Scan(&t.ID, &t.Title, &customerID, &t.DueDate, &t.Priority, &t.Status,
|
|
&repeat, &t.CreatedAt, &t.CustomerName); err != nil {
|
|
return nil, false
|
|
}
|
|
if customerID.Valid && customerID.String != "" {
|
|
t.CustomerID = &customerID.String
|
|
}
|
|
if repeat.Valid && repeat.String != "" {
|
|
t.Repeat = &repeat.String
|
|
}
|
|
return t, true
|
|
}
|
|
|
|
// pipelineStages 销售流程 8 阶段看板数据
|
|
func (s *Server) pipelineStages(w http.ResponseWriter, r *http.Request) {
|
|
uid := auth.UserID(r)
|
|
rows, err := s.db.Query("SELECT stage, company FROM customers WHERE owner_id=? AND deleted_at IS NULL", uid)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "查询失败")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
stageCompanies := map[string][]string{}
|
|
for rows.Next() {
|
|
var stage, company string
|
|
if err := rows.Scan(&stage, &company); err != nil {
|
|
continue
|
|
}
|
|
stageCompanies[stage] = append(stageCompanies[stage], company)
|
|
}
|
|
|
|
// 固定顺序输出 s1~s8
|
|
keys := make([]string, 0, len(models.StageNames))
|
|
for k := range models.StageNames {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
out := []map[string]any{}
|
|
for _, k := range keys {
|
|
out = append(out, map[string]any{
|
|
"stage": k,
|
|
"name": models.StageNames[k],
|
|
"count": len(stageCompanies[k]),
|
|
"companies": stageCompanies[k],
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// statsOverview 数据看板统计
|
|
func (s *Server) statsOverview(w http.ResponseWriter, r *http.Request) {
|
|
uid := auth.UserID(r)
|
|
|
|
type kv struct {
|
|
Key string `json:"key"`
|
|
Count int `json:"count"`
|
|
}
|
|
groupBy := func(table, col string) []kv {
|
|
rows, err := s.db.Query("SELECT "+col+", COUNT(*) FROM "+table+" WHERE owner_id=? AND deleted_at IS NULL GROUP BY "+col, uid)
|
|
if err != nil {
|
|
return []kv{}
|
|
}
|
|
defer rows.Close()
|
|
out := []kv{}
|
|
for rows.Next() {
|
|
var k kv
|
|
if rows.Scan(&k.Key, &k.Count) == nil {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
var totalCustomers, totalTasks, totalNotes, recentAdded int
|
|
_ = s.db.QueryRow("SELECT COUNT(*) FROM customers WHERE owner_id=? AND deleted_at IS NULL", uid).Scan(&totalCustomers)
|
|
_ = s.db.QueryRow("SELECT COUNT(*) FROM tasks WHERE owner_id=? AND deleted_at IS NULL", uid).Scan(&totalTasks)
|
|
_ = s.db.QueryRow("SELECT COUNT(*) FROM notes WHERE owner_id=? AND deleted_at IS NULL", uid).Scan(&totalNotes)
|
|
|
|
weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
|
|
_ = s.db.QueryRow("SELECT COUNT(*) FROM customers WHERE owner_id=? AND deleted_at IS NULL AND created_at>=?",
|
|
uid, weekAgo).Scan(&recentAdded)
|
|
|
|
// 阶段分布补全名称
|
|
stageDist := groupBy("customers", "stage")
|
|
for i := range stageDist {
|
|
if name, ok := models.StageNames[stageDist[i].Key]; ok {
|
|
stageDist[i].Key = name
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"totalCustomers": totalCustomers,
|
|
"totalTasks": totalTasks,
|
|
"totalNotes": totalNotes,
|
|
"recentAdded7d": recentAdded,
|
|
"levelDistribution": groupBy("customers", "level"),
|
|
"stageDistribution": stageDist,
|
|
"taskStatus": groupBy("tasks", "status"),
|
|
})
|
|
}
|