feat(settlement, admin): introduce new types and functions for downline share and settlement period hints

Added new types for downline share breakdown and settlement period open hints to enhance the agent settlement API. Updated the admin console components to support these new features, improving the user experience with better data presentation and interaction. Additionally, refined the date range field to accommodate new calendar markers and hints, ensuring a more intuitive interface for managing settlement periods.
This commit is contained in:
2026-06-12 16:01:42 +08:00
parent 1eb6702c51
commit 24fd7c10bd
50 changed files with 1821 additions and 618 deletions

View File

@@ -1,113 +1,158 @@
import type { AgentSettlementCycle } from "@/lib/agent-settlement-cycle";
const DATE_YMD_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
export type SettlementPeriodPresetKey = "this_week" | "last_week" | "this_month";
/** `datetime-local` 控件取值格式 */
export function toDateTimeLocalValue(date: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
function pad2(n: number): string {
return String(n).padStart(2, "0");
}
function startOfDay(date: Date): Date {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
/** 本地日历 `YYYY-MM-DD` */
export function toDateYmdValue(date: Date): string {
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
}
function endOfDay(date: Date): Date {
const d = new Date(date);
d.setHours(23, 59, 0, 0);
return d;
function parseDateYmd(value: string): Date | null {
const match = DATE_YMD_RE.exec(value.trim());
if (!match) {
return null;
}
const [, y, mo, d] = match;
const date = new Date(Number(y), Number(mo) - 1, Number(d));
return Number.isNaN(date.getTime()) ? null : date;
}
/** 周一为一周起始(与产品文档「周结」一致) */
function startOfWeekMonday(date: Date): Date {
const d = startOfDay(date);
const day = d.getDay();
const diff = day === 0 ? -6 : 1 - day;
d.setDate(d.getDate() + diff);
return d;
function formatUtcDateTimeFromMs(ms: number): string {
const date = new Date(ms);
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
}
function addDays(date: Date, days: number): Date {
const d = new Date(date);
d.setDate(d.getDate() + days);
/** 本地自然日 00:00:00 → UTC `YYYY-MM-DD HH:mm:ss`(开账 API */
export function localDateYmdToUtcPeriodStart(ymd: string): string {
const date = parseDateYmd(ymd);
if (date === null) {
return ymd;
}
const ms = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
0,
0,
0,
0,
).getTime();
return d;
return formatUtcDateTimeFromMs(ms);
}
function startOfMonth(date: Date): Date {
const d = startOfDay(date);
d.setDate(1);
/** 本地自然日 23:59:59 → UTC `YYYY-MM-DD HH:mm:ss`(开账 API */
export function localDateYmdToUtcPeriodEnd(ymd: string): string {
const date = parseDateYmd(ymd);
if (date === null) {
return ymd;
}
const ms = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
23,
59,
59,
999,
).getTime();
return d;
return formatUtcDateTimeFromMs(ms);
}
function endOfMonth(date: Date): Date {
const d = startOfDay(date);
d.setMonth(d.getMonth() + 1);
d.setDate(0);
return endOfDay(d);
}
export function settlementPeriodPresetRange(
key: SettlementPeriodPresetKey,
now: Date = new Date(),
export function localDateRangeToUtcPeriodBounds(
startYmd: string,
endYmd: string,
): { period_start: string; period_end: string } {
switch (key) {
case "this_week": {
const start = startOfWeekMonday(now);
const end = endOfDay(addDays(start, 6));
return {
period_start: toDateTimeLocalValue(start),
period_end: toDateTimeLocalValue(end),
};
}
case "last_week": {
const thisStart = startOfWeekMonday(now);
const start = addDays(thisStart, -7);
const end = endOfDay(addDays(start, 6));
return {
period_start: toDateTimeLocalValue(start),
period_end: toDateTimeLocalValue(end),
};
}
case "this_month": {
const start = startOfMonth(now);
const end = endOfMonth(now);
return {
period_start: toDateTimeLocalValue(start),
period_end: toDateTimeLocalValue(end),
};
}
}
return {
period_start: localDateYmdToUtcPeriodStart(startYmd),
period_end: localDateYmdToUtcPeriodEnd(endYmd),
};
}
/** 按代理结算周期推荐默认快捷开期(周结优先 */
export function defaultSettlementPeriodPreset(
cycle: AgentSettlementCycle,
): SettlementPeriodPresetKey {
if (cycle === "monthly") {
return "this_month";
/** UTC 账期时刻 → 本地日历 `YYYY-MM-DD`(列表展示 */
export function utcPeriodInstantToLocalYmd(iso: string | null | undefined): string {
if (iso == null || iso === "") {
return "—";
}
const ms = Date.parse(iso);
if (Number.isNaN(ms)) {
return iso.slice(0, 10);
}
return toDateYmdValue(new Date(ms));
}
return "this_week";
/** UTC 存储日 `Y-m-d` → 本地日历标记用 `yyyy-MM-dd` */
export function utcStorageDateToLocalMarkYmd(utcYmd: string): string {
const match = DATE_YMD_RE.exec(utcYmd.trim());
if (!match) {
return utcYmd;
}
const [, y, mo, d] = match;
const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), 12, 0, 0, 0);
return toDateYmdValue(new Date(ms));
}
export function utcStorageDatesToLocalMarks(dates: string[]): string[] {
return dates.map((day) => utcStorageDateToLocalMarkYmd(day));
}
/** 开账建议UTC 存储日 → 本地表单 `yyyy-MM-dd` */
export function utcStorageDateToLocalFormYmd(utcYmd: string): string {
return utcStorageDateToLocalMarkYmd(utcYmd);
}
export function formatSettlementPeriodSpan(
periodStart: string | undefined,
periodEnd: string | undefined,
): string {
const start = periodStart?.slice(0, 10) ?? "—";
const end = periodEnd?.slice(0, 10) ?? "—";
const start = utcPeriodInstantToLocalYmd(periodStart);
const end = utcPeriodInstantToLocalYmd(periodEnd);
return `${start} ~ ${end}`;
}
export function isSettlementLocalDateRangeValid(startYmd: string, endYmd: string): boolean {
const start = parseDateYmd(startYmd);
const end = parseDateYmd(endYmd);
if (start === null || end === null) {
return false;
}
return start.getTime() <= end.getTime();
}
function eachLocalYmdInRange(startYmd: string, endYmd: string): string[] {
const start = parseDateYmd(startYmd);
const end = parseDateYmd(endYmd);
if (start === null || end === null || start.getTime() > end.getTime()) {
return [];
}
const days: string[] = [];
const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
const endMs = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();
while (cursor.getTime() <= endMs) {
days.push(toDateYmdValue(cursor));
cursor.setDate(cursor.getDate() + 1);
}
return days;
}
/** 本地账期范围是否与已有账期日期重叠 */
export function settlementRangeOverlapsOccupiedDates(
startYmd: string,
endYmd: string,
occupiedLocalYmds: string[],
): boolean {
if (occupiedLocalYmds.length === 0) {
return false;
}
const occupied = new Set(occupiedLocalYmds);
return eachLocalYmdInRange(startYmd, endYmd).some((day) => occupied.has(day));
}