Files
subtrack/internal/web/templates/pages/calendar.html
T
subtrack a3fd817edc feat(v1.8): 包赛季模式、按次默认有效期、订阅删除
1) 包赛季(第三种计费方式)
- 套餐表单新增「包赛季」,套餐上直接填赛季开始日/结束日(必填,结束日不能早于开始日)
- 客户开通时自动带出赛季起止,不用再手填日期
- 日历上用横向色带标出整段赛季;赛季内任意一天点「完成」即整段完成
- 完成后不再进日历/待打,客户详情显示「赛季已完成 + 完成日期」
- 已过赛季结束日的套餐在客户开通下拉里置灰标「赛季已结束」,服务端同样拒绝开通

2) 按次默认有效期
- 套餐新增「按次默认有效期(天)」,开单时自动算出到期日(开始日 + 天数 − 1),仍可改

3) 订阅删除
- 已结束的订阅卡片在「重新启用」旁新增「删除」,二次确认后彻底删除该订阅及其打卡/追加/加急记录

4) 客户月历、日历与大屏同步识别赛季(色带、完成状态)

数据库兼容性:
- 仅新增列:packages.season / default_validity_days / season_start / season_end,subscriptions.season / season_done
- 没有改动原有 unit 的 CHECK 约束、没有重建表;老库启动自动 ALTER,可重复迁移

测试:
- 功能验证 13 项:赛季套餐创建、过期套餐不可选、开通带出日期、日历色带、完成整段后隐藏、按次自动到期、删除订阅
- 119 项验收回归全绿;go test ./... 通过;无头 Chrome 实拍 3 张(日历色带 / 客户赛季卡片 / 套餐表单)
- 版本号 1.7.0 → 1.8.0
2026-10-19 19:26:47 +08:00

270 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "content"}}
<div class="cal-page">
<header class="cal-head">
<div class="cal-title">
<a class="btn btn-ghost btn-square" href="/calendar?y={{.D.PrevY}}&m={{.D.PrevM}}&{{.D.FilterQuery}}" aria-label="上个月">‹</a>
<h1>{{.D.Title}}</h1>
<a class="btn btn-ghost btn-square" href="/calendar?y={{.D.NextY}}&m={{.D.NextM}}&{{.D.FilterQuery}}" aria-label="下个月">›</a>
<a class="btn btn-sm" href="/calendar?{{.D.FilterQuery}}">回到今天</a>
</div>
<div class="cal-stats">
<span><em>{{.D.MonthDue}}</em>本月到期</span>
<span><em>{{.D.MonthTasks}}</em>本月待打</span>
<span><em>{{.D.MonthDone}}</em>已完成</span>
{{if gt .D.MonthUrgent 0}}<span class="text-urgent"><em>{{.D.MonthUrgent}}</em>加急</span>{{end}}
<a class="btn btn-primary btn-sm" href="/customers/new">+ 新建客户</a>
</div>
</header>
{{if .D.HasTabs}}
<nav class="pkg-tabs">
{{range .D.Tabs}}
<a class="{{if .Active}}on{{end}}" style="--tab:{{.Color}}" href="{{.Href}}">
<i></i>{{.Name}}{{if gt .Count 0}}<em>{{.Count}}</em>{{end}}
</a>
{{end}}
</nav>
{{end}}
{{if .D.NoPackages}}
<div class="notice">还没有服务套餐,先去 <a href="/packages/new">新建套餐</a>,再给客户开通。</div>
{{else if .D.NoCustomers}}
<div class="notice">还没有客户,<a href="/customers/new">新建第一位客户</a>。</div>
{{end}}
<div class="cal-body">
<div class="cal-grid" role="grid">
<div class="cal-dow" role="row">
{{range .D.Weekdays}}<span>{{.}}</span>{{end}}
</div>
{{range .D.Weeks}}
<div class="cal-week" role="row">
{{range .}}
<a class="cal-cell heat-{{.Heat}}{{if not .InMonth}} out{{end}}{{if .IsToday}} today{{end}}{{if .IsSelected}} sel{{end}}{{if .IsWeekend}} weekend{{end}}{{if .DueCount}} has-due{{end}}"
href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{.Date}}&{{$.D.FilterQuery}}" role="gridcell">
{{if .HasSeason}}<span class="season-band{{if .SeasonStart}} start{{end}}{{if .SeasonEnd}} end{{end}}" style="--sc:{{.SeasonColor}}" aria-hidden="true"></span>{{end}}
{{if .Strip}}<span class="asg-strip" aria-hidden="true">{{range .Strip}}<i style="background:{{.}}"></i>{{end}}</span>{{end}}
<div class="cell-top">
<span class="dnum">{{.Day}}</span>
<span class="cell-badges">
{{if gt .UrgentCnt 0}}<span class="urgent-dot" title="{{.UrgentCnt}} 项加急"></span>{{end}}
{{if .DueCount}}<span class="due-badge"><em>到期</em>{{.DueCount}}</span>{{end}}
</span>
</div>
{{if .TaskCount}}
<div class="cell-count">
<b>{{.DoneCount}}</b><span>/ {{.TaskCount}}</span>
</div>
{{end}}
</a>
{{end}}
</div>
{{end}}
</div>
<aside class="drawer">
<div class="drawer-clock">
<b data-clock>--:--:--</b>
<span>{{.D.TodayStr}}</span>
</div>
<header class="drawer-head">
<h2>{{.D.SelectedLabel}}</h2>
<div class="drawer-tags">
{{if .D.SelectedIsToday}}<span class="chip chip-active">今天</span>{{end}}
{{if .D.SelectedIsPast}}<span class="chip chip-closed">已过去</span>{{end}}
<span class="chip chip-plain">{{.D.TaskPending}} 待完成</span>
</div>
</header>
<section class="drawer-block">
<h3>当日待完成 <em>{{.D.TaskPending}}</em><span class="muted small">/ {{len .D.Tasks}} 单</span></h3>
<form class="quick-form" method="post" action="/checkins/quick">
<input type="hidden" name="date" value="{{.D.Selected}}">
<input type="hidden" name="pkg" value="{{.D.PkgID}}">
<input type="hidden" name="tag" value="{{.D.Tag}}">
<input type="hidden" name="back" value="{{.D.Back}}">
<input type="text" name="tail" placeholder="输入手机尾号,回车即可完成" maxlength="8" autocomplete="off" inputmode="numeric" aria-label="手机尾号快速完成">
<button class="btn btn-sm btn-primary" type="submit">完成</button>
<button type="button" class="btn btn-sm btn-ghost keypad-toggle" data-keypad-toggle title="切换触控/PC 模式" aria-label="切换触控/PC 模式">⌨</button>
</form>
<p class="muted small quick-hint">唯一匹配直接完成;同尾号多人 / 一单多服务会列出让你勾选。</p>
<div class="keypad" data-keypad role="group" aria-label="数字小键盘" hidden>
<button type="button" class="key" data-key="1">1</button>
<button type="button" class="key" data-key="2">2</button>
<button type="button" class="key" data-key="3">3</button>
<button type="button" class="key" data-key="4">4</button>
<button type="button" class="key" data-key="5">5</button>
<button type="button" class="key" data-key="6">6</button>
<button type="button" class="key" data-key="7">7</button>
<button type="button" class="key" data-key="8">8</button>
<button type="button" class="key" data-key="9">9</button>
<button type="button" class="key key-fn" data-key="back" aria-label="退格">⌫</button>
<button type="button" class="key" data-key="0">0</button>
<button type="button" class="key key-ok" data-key="ok" aria-label="确认完成">✓</button>
</div>
{{if .D.PickTail}}
<section class="picker">
<header class="picker-head">
<h4>尾号 {{.D.PickTail}} 的待完成项</h4>
<a class="btn btn-sm btn-ghost" href="{{.D.Back}}">收起</a>
</header>
{{if .D.PickRows}}
<form method="post" action="/checkins/batch">
<input type="hidden" name="date" value="{{.D.Selected}}">
<input type="hidden" name="back" value="{{.D.Back}}">
<ul class="pick-list">
{{range .D.PickRows}}
<li>
<label>
<input type="checkbox" name="sub_ids" value="{{.ID}}"{{if not .Done}}{{if not .Skipped}} checked{{end}}{{end}}>
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pick-pkg">{{.PackageName}}</span>
<span class="pick-remain">{{.RemainShort}}</span>
{{if .Done}}<span class="chip chip-active">已完成</span>{{end}}
{{if .Skipped}}<span class="chip chip-paused">已跳过</span>{{end}}
</label>
</li>
{{end}}
</ul>
<div class="actions">
<button class="btn btn-primary btn-sm" type="submit">确认完成选中项</button>
<a class="btn btn-sm" href="{{.D.Back}}">取消</a>
</div>
</form>
{{else}}
<p class="muted">{{.D.PickMsg}}</p>
{{end}}
</section>
{{end}}
{{if .D.Tasks}}
<ul class="task-list">
{{range .D.Tasks}}
{{if or .Done .Skipped}}
<li class="task task-done" style="--asg:{{.AssigneeHex}}">
<details class="done-fold">
<summary>
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
<span class="chip {{if .Done}}chip-active{{else}}chip-paused{{end}}">{{if .Done}}已完成{{else}}已跳过{{end}}</span>
{{if .Urgent}}<span class="tag-urgent">加急</span>{{end}}
<span class="fold-caret" aria-hidden="true"></span>
</summary>
<div class="done-detail">
<div class="task-meta">
<span class="asg{{if .AssigneeNone}} none{{end}}"><i style="background:{{.AssigneeHex}}"></i>{{.AssigneeLabel}}</span>
<span>{{.RemainText}}</span>
{{if .DueText}}<span class="due due-{{.Eval.Level}}">{{.DueText}}</span>{{end}}
</div>
{{if .Note}}<div class="task-note">{{.Note}}{{if .Operator}} · {{.Operator}}{{end}}</div>{{end}}
<div class="task-actions">
<form method="post" action="/checkins/{{.CheckinID}}/delete">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-ghost" type="submit">撤销打卡</button>
</form>
<details class="more-actions">
<summary class="btn btn-sm btn-ghost" title="更多操作" aria-label="更多操作">⋯</summary>
<div class="more-menu">
<form method="post" action="/tasks/urgent">
<input type="hidden" name="sub_id" value="{{.ID}}">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="urgent" value="{{if .Urgent}}0{{else}}1{{end}}">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm {{if .Urgent}}btn-urgent{{else}}btn-ghost{{end}}" type="submit">{{if .Urgent}}取消加急{{else}}标记加急{{end}}</button>
</form>
<a class="btn btn-sm" href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{$.D.Selected}}&{{$.D.FilterQuery}}&extend={{.ID}}#extend">追加时长</a>
</div>
</details>
</div>
</div>
</details>
</li>
{{else}}
<li class="task{{if .Urgent}} urgent{{end}}" style="--asg:{{.AssigneeHex}}">
<div class="task-main">
<a class="task-title" href="/customers/{{.CustomerID}}">
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
{{if .Urgent}}<span class="tag-urgent">加急</span>{{end}}
</a>
<div class="task-meta">
<span class="asg{{if .AssigneeNone}} none{{end}}"><i style="background:{{.AssigneeHex}}"></i>{{.AssigneeLabel}}</span>
<span>{{.RemainText}}</span>
{{if .DueText}}<span class="due due-{{.Eval.Level}}">{{.DueText}}</span>{{end}}
</div>
{{if .Note}}<div class="task-note">{{.Note}}{{if .Operator}} · {{.Operator}}{{end}}</div>{{end}}
</div>
<div class="task-actions">
<form method="post" action="/subscriptions/{{.ID}}/checkin">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="status" value="done">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-primary">完成</button>
</form>
<details class="more-actions">
<summary class="btn btn-sm btn-ghost" title="更多操作" aria-label="更多操作">⋯</summary>
<div class="more-menu">
<form method="post" action="/subscriptions/{{.ID}}/checkin">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="status" value="skipped">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-ghost" type="submit">跳过这次</button>
</form>
<form method="post" action="/tasks/urgent">
<input type="hidden" name="sub_id" value="{{.ID}}">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="urgent" value="{{if .Urgent}}0{{else}}1{{end}}">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm {{if .Urgent}}btn-urgent{{else}}btn-ghost{{end}}" type="submit">{{if .Urgent}}取消加急{{else}}标记加急{{end}}</button>
</form>
<a class="btn btn-sm" href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{$.D.Selected}}&{{$.D.FilterQuery}}&extend={{.ID}}#extend">追加时长</a>
</div>
</details>
</div>
</li>
{{end}}
{{end}}
</ul>
{{else}}<p class="muted">这天没有待打任务。</p>{{end}}
</section>
{{if .D.Extend}}{{template "extend-panel" .D.Extend}}{{end}}
<section class="drawer-block">
<h3>这天到期 <em>{{len .D.Due}}</em></h3>
{{if .D.Due}}
<ul class="due-list">
{{range .D.Due}}
<li>
<a class="due-main" href="/customers/{{.CustomerID}}">
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
<span class="rem">{{.RemainShort}}</span>
</a>
<a class="btn btn-sm" href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{$.D.Selected}}&{{$.D.FilterQuery}}&extend={{.ID}}#extend">追加</a>
</li>
{{end}}
</ul>
{{else}}<p class="muted">没有到期订阅。</p>{{end}}
</section>
<section class="drawer-block">
<h3>近 7 天到期</h3>
{{if .D.OtherDue}}
<ul class="mini-due">
{{range .D.OtherDue}}
<li>
<a href="/customers/{{.CustomerID}}"><span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span></a>
<span class="muted">{{.PackageName}}</span>
<span class="sm-date">{{.EndDate}}</span>
</li>
{{end}}
</ul>
{{else}}<p class="muted">7 天内没有其它到期。</p>{{end}}
</section>
</aside>
</div>
</div>
{{end}}