feat(proj): init
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
/** Mirrors PleasePayMe.Application.Jobs.PaySchedule for calendar rendering. */
|
||||
|
||||
export type WeekendPolicy = "before_weekend" | "after_weekend";
|
||||
|
||||
export type PayOccurrence = {
|
||||
date: string;
|
||||
jobId: number;
|
||||
jobName: string;
|
||||
scheduledDay: number;
|
||||
percent: number;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
function pad2(n: number): string {
|
||||
return String(n).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function toIso(year: number, month: number, day: number): string {
|
||||
return `${year}-${pad2(month)}-${pad2(day)}`;
|
||||
}
|
||||
|
||||
export function parseIso(iso: string): { year: number; month: number; day: number } {
|
||||
const [year, month, day] = iso.slice(0, 10).split("-").map(Number);
|
||||
return { year, month, day };
|
||||
}
|
||||
|
||||
export function addDaysIso(iso: string, delta: number): string {
|
||||
const { year, month, day } = parseIso(iso);
|
||||
const dt = new Date(Date.UTC(year, month - 1, day + delta));
|
||||
return toIso(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate());
|
||||
}
|
||||
|
||||
export function weekdayMon0(iso: string): number {
|
||||
const { year, month, day } = parseIso(iso);
|
||||
// 0 = Mon … 6 = Sun
|
||||
const js = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
||||
return (js + 6) % 7;
|
||||
}
|
||||
|
||||
export function adjustForWeekend(iso: string, policy: WeekendPolicy): string {
|
||||
const wd = weekdayMon0(iso);
|
||||
// Mon=0 … Sat=5, Sun=6
|
||||
if (wd === 5) {
|
||||
return policy === "before_weekend" ? addDaysIso(iso, -1) : addDaysIso(iso, 2);
|
||||
}
|
||||
if (wd === 6) {
|
||||
return policy === "before_weekend" ? addDaysIso(iso, -2) : addDaysIso(iso, 1);
|
||||
}
|
||||
return iso;
|
||||
}
|
||||
|
||||
export function nominalDate(year: number, month: number, dayOfMonth: number): string {
|
||||
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
const actualDay = Math.min(dayOfMonth, daysInMonth);
|
||||
return toIso(year, month, actualDay);
|
||||
}
|
||||
|
||||
export function paysInRange(
|
||||
jobs: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
salary_amount: number;
|
||||
pay_days: number[];
|
||||
first_pay_percent: number;
|
||||
weekend_policy: WeekendPolicy;
|
||||
is_active: boolean;
|
||||
}>,
|
||||
rangeStart: string,
|
||||
rangeEnd: string,
|
||||
): PayOccurrence[] {
|
||||
const start = parseIso(rangeStart);
|
||||
const end = parseIso(rangeEnd);
|
||||
const result: PayOccurrence[] = [];
|
||||
|
||||
for (const job of jobs) {
|
||||
if (!job.is_active || job.pay_days.length === 0) continue;
|
||||
|
||||
const ordered = [...job.pay_days].sort((a, b) => a - b).slice(0, 2);
|
||||
const firstPercent = ordered.length <= 1 ? 100 : job.first_pay_percent;
|
||||
const secondPercent = ordered.length === 1 ? 0 : 100 - firstPercent;
|
||||
|
||||
// Extra months: weekend shift can move pay across month boundary.
|
||||
let cursor = new Date(Date.UTC(start.year, start.month - 2, 1));
|
||||
const last = new Date(Date.UTC(end.year, end.month, 1));
|
||||
|
||||
while (cursor <= last) {
|
||||
const year = cursor.getUTCFullYear();
|
||||
const month = cursor.getUTCMonth() + 1;
|
||||
|
||||
ordered.forEach((scheduledDay, i) => {
|
||||
const nominal = nominalDate(year, month, scheduledDay);
|
||||
const actual = adjustForWeekend(nominal, job.weekend_policy);
|
||||
if (actual < rangeStart || actual > rangeEnd) return;
|
||||
|
||||
const percent = i === 0 ? firstPercent : secondPercent;
|
||||
const amount = Math.round((job.salary_amount * percent) / 100 * 100) / 100;
|
||||
result.push({
|
||||
date: actual,
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
scheduledDay,
|
||||
percent,
|
||||
amount,
|
||||
});
|
||||
});
|
||||
|
||||
cursor = new Date(Date.UTC(year, month, 1));
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort((a, b) =>
|
||||
a.date === b.date
|
||||
? a.jobId - b.jobId || a.scheduledDay - b.scheduledDay
|
||||
: a.date < b.date
|
||||
? -1
|
||||
: 1,
|
||||
);
|
||||
}
|
||||
|
||||
export function monthGrid(year: number, month: number): string[] {
|
||||
const first = toIso(year, month, 1);
|
||||
const startOffset = weekdayMon0(first);
|
||||
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
||||
const cells: string[] = [];
|
||||
|
||||
for (let i = 0; i < startOffset; i++) {
|
||||
cells.push(addDaysIso(first, i - startOffset));
|
||||
}
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
cells.push(toIso(year, month, d));
|
||||
}
|
||||
while (cells.length % 7 !== 0 || cells.length < 35) {
|
||||
const last = cells[cells.length - 1]!;
|
||||
cells.push(addDaysIso(last, 1));
|
||||
if (cells.length >= 42) break;
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
Reference in New Issue
Block a user