Initial commit - Route Commerce platform
This commit is contained in:
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user