1) 日历计数口径
- 工作台与大屏的格子统一改为「已完成 / 总数」(如 2/5 = 共 5 单、做完 2 单)
2) 客户详情·当月排单
- 新增客户专属月历:每天显示 已完成/总数,可翻月,点某天跳转到日历对应日期
- 没有排单的日期淡化显示
3) 套餐标签与页签合并
- packages 新增 tag 列(可空),套餐编辑页可填
- 日历/大屏页签按标签合并:同标签套餐合成一个页签;勾了「独立显示」的套餐额外再开一个自己的页签
- 大屏同样支持标签筛选,链接参数为 tag=xxx
4) 批量开通
- 新增套餐详情页 /packages/{id}:显示套餐信息 + 客户列表
- 表头「全选」置顶,已订阅该套餐的客户置灰不可选;开始日期默认今天、天数/次数默认取套餐设置,均可改
- POST /packages/{id}/subscribe 批量开通,重复的自动跳过并汇总提示
- 修复 r.PostForm 未 ParseForm 导致拿不到勾选项的问题(批量打卡同源问题一并修正)
5) 触控 / PC 模式
- 尾号输入框右侧新增切换按钮,触控模式展开 3×4 键盘、输入框只读 + inputmode=none(不唤起系统输入法),按键直接写值
- 偏好记在 localStorage,默认 PC 模式(键盘收起)
6) 按次到期
- 按次订阅的到期日保持选填;填了到期后状态显示「已到期」且不再进日历待排(复用既有 end_date,无新列)
数据库兼容性:
- 只新增 packages.tag 一列,老库启动时自动 ALTER,可重复迁移;原有表结构与接口不变
测试:
- 无头 Chrome 实拍 5 张页面(日历/触控模式/客户月历/套餐批量/大屏)
- 功能验证 18 项全过(计数口径、标签页签、标签筛选、批量开通与重复跳过、月历翻月、触控开关、按次过期不上日历)
- 119 项验收回归全绿;go vet / go test ./... 通过;版本号 1.7.0
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -27,13 +28,16 @@ func (s *Server) handlePackages(w http.ResponseWriter, r *http.Request) {
|
||||
type packageFormData struct {
|
||||
Package *store.Package
|
||||
IsNew bool
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func (s *Server) handlePackageNewPage(w http.ResponseWriter, r *http.Request) {
|
||||
p := s.newPage(r, "新增套餐", "packages")
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{
|
||||
Package: &store.Package{Unit: domain.UnitDay, DefaultAmount: 30, Color: "#2F5D50"},
|
||||
IsNew: true,
|
||||
Tags: tags,
|
||||
}
|
||||
s.render(w, r, "package_form", p)
|
||||
}
|
||||
@@ -63,6 +67,7 @@ func readPackageForm(r *http.Request) *store.Package {
|
||||
p.Unit = domain.UnitDay
|
||||
}
|
||||
p.CalendarSeparate = r.PostFormValue("calendar_separate") != ""
|
||||
p.Tag = strings.TrimSpace(r.PostFormValue("tag"))
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -85,17 +90,123 @@ func validatePackage(p *store.Package) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// packageDetailData 套餐详情页数据。
|
||||
type packageDetailData struct {
|
||||
Package *store.Package
|
||||
Customers []store.Customer
|
||||
Subscribed map[int64]bool
|
||||
Q string
|
||||
Tags []string
|
||||
Today string
|
||||
}
|
||||
|
||||
// handlePackageDetail 套餐详情:查看信息 + 勾选客户批量开通。
|
||||
func (s *Server) handlePackageDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id := pathID(r)
|
||||
pkg, err := s.db.Package(id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
today := domain.Today()
|
||||
warn := s.warnDays()
|
||||
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
list, _, err := s.db.ListCustomers(q, 300, 0, domain.Date(today), warn, false)
|
||||
if err != nil {
|
||||
http.Error(w, "读取客户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
subscribed, err := s.db.SubscribedCustomerIDs(id)
|
||||
if err != nil {
|
||||
http.Error(w, "读取订阅失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p := s.newPage(r, "套餐详情", "packages")
|
||||
p.D = &packageDetailData{Package: pkg, Customers: list, Subscribed: subscribed, Q: q, Tags: tags, Today: domain.Date(today)}
|
||||
s.render(w, r, "package_detail", p)
|
||||
}
|
||||
|
||||
// handlePackageBatchSubscribe 给勾选的客户批量开通该套餐。
|
||||
func (s *Server) handlePackageBatchSubscribe(w http.ResponseWriter, r *http.Request) {
|
||||
id := pathID(r)
|
||||
pkg, err := s.db.Package(id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
back := "/packages/" + itoa(id)
|
||||
raw := r.PostForm["customer_ids"]
|
||||
if len(raw) == 0 {
|
||||
s.redirect(w, r, back, "没有选中任何客户")
|
||||
return
|
||||
}
|
||||
start := strings.TrimSpace(r.PostFormValue("start_date"))
|
||||
if start == "" {
|
||||
start = domain.Date(domain.Today())
|
||||
}
|
||||
if _, err := domain.ParseDate(start); err != nil {
|
||||
s.redirect(w, r, back, "开始日期格式不正确")
|
||||
return
|
||||
}
|
||||
end := strings.TrimSpace(r.PostFormValue("end_date"))
|
||||
days := s.formInt(r, "days", 0)
|
||||
count := s.formInt(r, "total_count", 0)
|
||||
u := s.currentUser(r)
|
||||
var op int64
|
||||
if u != nil {
|
||||
op = u.ID
|
||||
}
|
||||
|
||||
done, skipped, failed := 0, 0, 0
|
||||
var lastErr string
|
||||
for _, v := range raw {
|
||||
cid, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
||||
if err != nil {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
_, err = s.db.CreateSub(store.CreateSubInput{
|
||||
CustomerID: cid, PackageID: id, StartDate: start, EndDate: end,
|
||||
Days: days, TotalCount: count, OperatorID: op,
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "已订阅") {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
failed++
|
||||
lastErr = err.Error()
|
||||
continue
|
||||
}
|
||||
done++
|
||||
}
|
||||
s.logOp(r, "批量开通", "package", id,
|
||||
fmt.Sprintf("%s:成功 %d、跳过 %d、失败 %d", pkg.Name, done, skipped, failed))
|
||||
msg := fmt.Sprintf("已为 %d 位客户开通「%s」", done, pkg.Name)
|
||||
if skipped > 0 {
|
||||
msg += fmt.Sprintf(",%d 位已有该套餐已跳过", skipped)
|
||||
}
|
||||
if failed > 0 {
|
||||
msg += fmt.Sprintf(",%d 位失败(%s)", failed, lastErr)
|
||||
}
|
||||
s.redirect(w, r, back, msg)
|
||||
}
|
||||
|
||||
func (s *Server) handlePackageCreate(w http.ResponseWriter, r *http.Request) {
|
||||
pkg := readPackageForm(r)
|
||||
p := s.newPage(r, "新增套餐", "packages")
|
||||
if msg := validatePackage(pkg); msg != "" {
|
||||
p.D = &packageFormData{Package: pkg, IsNew: true}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{Package: pkg, IsNew: true, Tags: tags}
|
||||
s.fail(w, r, "package_form", msg, p)
|
||||
return
|
||||
}
|
||||
id, err := s.db.CreatePackage(pkg)
|
||||
if err != nil {
|
||||
p.D = &packageFormData{Package: pkg, IsNew: true}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{Package: pkg, IsNew: true, Tags: tags}
|
||||
s.fail(w, r, "package_form", "保存失败:"+err.Error(), p)
|
||||
return
|
||||
}
|
||||
@@ -111,7 +222,8 @@ func (s *Server) handlePackageEditPage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
p := s.newPage(r, "编辑套餐", "packages")
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags}
|
||||
s.render(w, r, "package_form", p)
|
||||
}
|
||||
|
||||
@@ -121,12 +233,14 @@ func (s *Server) handlePackageUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
pkg.ID = id
|
||||
p := s.newPage(r, "编辑套餐", "packages")
|
||||
if msg := validatePackage(pkg); msg != "" {
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags}
|
||||
s.fail(w, r, "package_form", msg, p)
|
||||
return
|
||||
}
|
||||
if err := s.db.UpdatePackage(pkg); err != nil {
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false}
|
||||
tags, _ := s.db.PackageTags()
|
||||
p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags}
|
||||
s.fail(w, r, "package_form", "保存失败:"+err.Error(), p)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user