Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
/**
* Water Log reporting - shapes entries for export, sync, and display.
*/
export type WaterLogReportRow = {
logged_at: string;
headgate_name: string;
user_name: string;
user_role: string;
measurement: number;
unit: string;
notes: string | null;
submitted_via: string;
};
export type WaterLogFilter = {
dateFrom?: string;
dateTo?: string;
headgateId?: string;
userId?: string;
submittedVia?: string;
};
export type IrrigationSeasonSettings = {
seasonStartMonth: number;
seasonStartDay: number;
seasonEndMonth: number;
seasonEndDay: number;
};
export type DailyReportOptions = {
recipientName?: string;
seasonLabel?: string;
sendEvenIfEmpty?: boolean;
previewMode?: boolean;
};
export const WATER_LOG_DISPLAY_REFRESH_MS = 30_000;
export function getDisplayAgeLabel(minutes: number | null): string {
if (minutes === null) return "never";
if (minutes < 1) return "just now";
if (minutes < 60) return Math.floor(minutes) + "m ago";
if (minutes < 120) return "1h ago";
if (minutes < 1440) return Math.floor(minutes / 60) + "h ago";
return Math.floor(minutes / 1440) + "d ago";
}
export function getDisplayAgeColor(minutes: number | null): "green" | "yellow" | "red" {
if (minutes === null) return "red";
if (minutes < 30) return "green";
if (minutes < 120) return "yellow";
return "red";
}
export function shapeWaterLogEntry(entry: {
id: string;
logged_at: string;
headgate_name: string;
user_name: string;
measurement: number;
unit: string;
notes: string | null;
submitted_via: string;
user_id?: string;
[key: string]: unknown;
}): WaterLogReportRow {
return {
logged_at: entry.logged_at,
headgate_name: entry.headgate_name,
user_name: entry.user_name,
user_role: (entry.user_role as string) ?? "irrigator",
measurement: entry.measurement,
unit: entry.unit,
notes: entry.notes ?? null,
submitted_via: entry.submitted_via,
};
}
export function filterWaterLogEntries(
rows: WaterLogReportRow[],
filters: WaterLogFilter
): WaterLogReportRow[] {
return rows.filter((row) => {
if (filters.dateFrom && row.logged_at < filters.dateFrom) return false;
if (filters.dateTo) {
const toEnd = filters.dateTo + "T23:59:59.999Z";
if (row.logged_at > toEnd) return false;
}
if (filters.headgateId && row.headgate_name !== filters.headgateId) return false;
if (filters.userId && row.user_name !== filters.userId) return false;
if (filters.submittedVia && row.submitted_via !== filters.submittedVia) return false;
return true;
});
}
function csvEncode(value: string): string {
return '"' + value.replace(/"/g, '""') + '"';
}
export function waterLogToCSV(rows: WaterLogReportRow[]): string {
const headers = ["When", "Headgate", "User", "Role", "Measurement", "Unit", "Notes", "Via"];
const lines: string[] = [headers.join(",")];
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const cells = [
csvEncode(r.logged_at),
csvEncode(r.headgate_name),
csvEncode(r.user_name),
csvEncode(r.user_role),
String(r.measurement),
csvEncode(r.unit),
csvEncode(r.notes ?? ""),
csvEncode(r.submitted_via),
];
lines.push(cells.join(","));
}
return lines.join("\n");
}
export function downloadWaterLogCSV(filename: string, rows: WaterLogReportRow[]): void {
const csv = waterLogToCSV(rows);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function isInIrrigationSeason(
date: Date,
settings: IrrigationSeasonSettings = {
seasonStartMonth: 3,
seasonStartDay: 15,
seasonEndMonth: 10,
seasonEndDay: 15,
}
): boolean {
const month = date.getMonth() + 1;
const day = date.getDate();
const start = settings.seasonStartMonth * 100 + settings.seasonStartDay;
const end = settings.seasonEndMonth * 100 + settings.seasonEndDay;
const current = month * 100 + day;
if (start <= end) {
return current >= start && current <= end;
} else {
return current >= start || current <= end;
}
}
export function formatDailyWaterReport(
rows: WaterLogReportRow[],
opts: DailyReportOptions = {}
): string | null {
const today = new Date();
const months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
const dateLabel = months[today.getMonth()] + " " + today.getDate() + ", " + today.getFullYear();
const count = rows.length;
if (count === 0) {
if (!opts.sendEvenIfEmpty) return null;
return "Water Log Summary - " + dateLabel + "\nNo entries reported today.\n\nSent by Route Commerce Water Log";
}
let total = 0;
for (let j = 0; j < rows.length; j++) {
total += rows[j].measurement;
}
const lines: string[] = [
"Water Log Summary - " + dateLabel,
count + " " + (count === 1 ? "entry" : "entries") + " reported today (" + total.toFixed(2) + " total)",
"",
];
for (let k = 0; k < rows.length; k++) {
const row = rows[k];
const time = new Date(row.logged_at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
let entryLine = time + " | " + row.user_name + " | " + row.headgate_name + " | " + row.measurement + " " + row.unit;
if (row.notes) {
entryLine += ' - "' + row.notes + '"';
}
lines.push(entryLine);
}
lines.push("", "Sent by Route Commerce Water Log");
return lines.join("\n");
}