157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
import { writeFileSync } from "node:fs"
|
||
import { Openmagicline } from "openmagicline"
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Config – set via environment variables or edit directly below
|
||
// ---------------------------------------------------------------------------
|
||
const GYM = process.env.MAGICLINE_GYM ?? "dieheldin"
|
||
const USERNAME = process.env.MAGICLINE_USER ?? "tanja.wild"
|
||
const PASSWORD = process.env.MAGICLINE_PASS ?? "Luna22111993"
|
||
|
||
if (!GYM || !USERNAME || !PASSWORD) {
|
||
console.error(
|
||
"Missing credentials. Set MAGICLINE_GYM, MAGICLINE_USER, and MAGICLINE_PASS.",
|
||
)
|
||
process.exit(1)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Escape a value for use in a CSV cell. */
|
||
function csvCell(value: unknown): string {
|
||
if (value === null || value === undefined) return ""
|
||
const str = String(value)
|
||
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
||
return `"${str.replace(/"/g, '""')}"`
|
||
}
|
||
return str
|
||
}
|
||
|
||
/** Escape a value for use in HTML table cell content. */
|
||
function htmlEscape(value: unknown): string {
|
||
if (value === null || value === undefined) return ""
|
||
return String(value)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Main
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const mgl = new Openmagicline({ gym: GYM, username: USERNAME, password: PASSWORD })
|
||
await mgl.login()
|
||
console.log("Logged in.")
|
||
|
||
// Fetch the real facility/organization unit ID — searching with facility:0 returns nothing.
|
||
const facilityId = await mgl.util.getDefaultUnitID()
|
||
console.log(`Using facility ID: ${facilityId}`)
|
||
|
||
const SEARCH_CAP = 50
|
||
|
||
const seen = new Set<number>()
|
||
const customers: Record<string, unknown>[] = []
|
||
|
||
async function sweep(prefix: string): Promise<void> {
|
||
const results = await mgl.customer.search(prefix, {
|
||
facility: facilityId,
|
||
showAllFacilities: false,
|
||
searchInName: false,
|
||
searchInCustomerNumber: true,
|
||
})
|
||
|
||
if (results.length === SEARCH_CAP) {
|
||
for (const d of "0123456789") {
|
||
await sweep(prefix + d)
|
||
}
|
||
return
|
||
}
|
||
|
||
for (const c of results) {
|
||
if (!seen.has(c.databaseId) && c.customerStatus === 1) {
|
||
seen.add(c.databaseId)
|
||
customers.push(c as unknown as Record<string, unknown>)
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const d of "0123456789") {
|
||
const prefix = `1-${d}`
|
||
process.stdout.write(`Sweeping '${prefix}'… `)
|
||
const before = customers.length
|
||
await sweep(prefix)
|
||
console.log(`+${customers.length - before} (total: ${customers.length})`)
|
||
}
|
||
|
||
if (customers.length === 0) {
|
||
console.error("No customers found.")
|
||
process.exit(1)
|
||
}
|
||
|
||
// Collect the union of all keys across all customer records
|
||
const allKeys = Array.from(
|
||
customers.reduce((keys, c) => {
|
||
for (const k of Object.keys(c)) keys.add(k)
|
||
return keys
|
||
}, new Set<string>()),
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CSV export
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const csvLines = [
|
||
allKeys.map(csvCell).join(","),
|
||
...customers.map((c) => allKeys.map((k) => csvCell(c[k])).join(",")),
|
||
]
|
||
const csvPath = "customers.csv"
|
||
writeFileSync(csvPath, csvLines.join("\n"), "utf-8")
|
||
console.log(`CSV written to ${csvPath} (${customers.length} rows)`)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// HTML export
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const headerCells = allKeys.map((k) => `<th>${htmlEscape(k)}</th>`).join("")
|
||
const bodyRows = customers
|
||
.map(
|
||
(c) =>
|
||
`<tr>${allKeys.map((k) => `<td>${htmlEscape(c[k])}</td>`).join("")}</tr>`,
|
||
)
|
||
.join("\n")
|
||
|
||
const html = `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Customers – ${new Date().toLocaleDateString()}</title>
|
||
<style>
|
||
body { font-family: sans-serif; font-size: 13px; padding: 1rem; }
|
||
h1 { margin-bottom: 0.5rem; }
|
||
p.meta { color: #666; margin-bottom: 1rem; }
|
||
table { border-collapse: collapse; width: 100%; }
|
||
th, td { border: 1px solid #ccc; padding: 4px 8px; text-align: left; white-space: nowrap; }
|
||
th { background: #f0f0f0; position: sticky; top: 0; }
|
||
tr:nth-child(even) { background: #fafafa; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Customer Export</h1>
|
||
<p class="meta">Generated: ${new Date().toISOString()} | Total: ${customers.length}</p>
|
||
<table>
|
||
<thead><tr>${headerCells}</tr></thead>
|
||
<tbody>
|
||
${bodyRows}
|
||
</tbody>
|
||
</table>
|
||
</body>
|
||
</html>`
|
||
|
||
const htmlPath = "customers.html"
|
||
writeFileSync(htmlPath, html, "utf-8")
|
||
console.log(`HTML written to ${htmlPath}`)
|