Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da64562eb4 |
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
@@ -0,0 +1,8 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.0.0 (2026-05-24)
|
||||
|
||||
- Eerste commit — Express API voor Padel Planner
|
||||
- SQLite database met players, matches, scores
|
||||
- REST endpoints: /players, /matches, /scores
|
||||
- PIN authenticatie voor spelers
|
||||
@@ -75,9 +75,6 @@ function migrateSchema() {
|
||||
CREATE INDEX IF NOT EXISTS idx_matchp_match ON match_players(match_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_matchp_player ON match_players(player_id);
|
||||
|
||||
-- Migration: match type kolom
|
||||
ALTER TABLE matches ADD COLUMN match_type TEXT NOT NULL DEFAULT 'friendly';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
player_id TEXT REFERENCES players(id) ON DELETE SET NULL,
|
||||
@@ -252,8 +249,8 @@ function addMatch(match) {
|
||||
const db = getDb();
|
||||
const id = match.id || String(Date.now());
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO matches (id, status, responses, proposed_at, date, start, end, location, day, score, booker_id, booker_name, match_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO matches (id, status, responses, proposed_at, date, start, end, location, day, score, booker_id, booker_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, match.status || 'proposed',
|
||||
JSON.stringify(match.responses || {}),
|
||||
@@ -263,8 +260,7 @@ function addMatch(match) {
|
||||
match.day || null,
|
||||
typeof match.score === 'object' ? JSON.stringify(match.score) : (match.score || null),
|
||||
match.booker_id || null,
|
||||
match.booker_name || null,
|
||||
match.match_type || 'friendly');
|
||||
match.booker_name || null);
|
||||
|
||||
if (Array.isArray(match.players)) {
|
||||
const pstmt = db.prepare(`INSERT INTO match_players (match_id, player_id, team, score) VALUES (?, ?, ?, ?)`);
|
||||
@@ -290,11 +286,11 @@ function updateMatch(id, match) {
|
||||
// Build SET dynamically to only update provided fields
|
||||
const fields = [];
|
||||
const values = [];
|
||||
for (const key of ['status', 'responses', 'proposed_at', 'date', 'start', 'end', 'location', 'day', 'score', 'booker_id', 'booker_name', 'proposed_teams', 'match_type']) {
|
||||
for (const key of ['status', 'responses', 'proposed_at', 'date', 'start', 'end', 'location', 'day', 'score', 'booker_id', 'booker_name']) {
|
||||
if (match[key] !== undefined) {
|
||||
fields.push(`${key}=?`);
|
||||
let val = merged[key];
|
||||
if (key === 'responses' || key === 'score' || key === 'proposed_teams') {
|
||||
if (key === 'responses' || (key === 'score' && typeof val === 'object' && val !== null)) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
values.push(key === 'booker_id' || key === 'booker_name' ? String(val) : val);
|
||||
@@ -450,11 +446,6 @@ function rowToMatch(r) {
|
||||
try { score = JSON.parse(r.score); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
let proposed_teams = null;
|
||||
if (r.proposed_teams) {
|
||||
try { proposed_teams = JSON.parse(r.proposed_teams); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
status: r.status,
|
||||
@@ -466,8 +457,6 @@ function rowToMatch(r) {
|
||||
location: r.location,
|
||||
day: r.day,
|
||||
score: score,
|
||||
proposed_teams: proposed_teams,
|
||||
match_type: r.match_type || 'friendly',
|
||||
booker_id: r.booker_id || null,
|
||||
booker_name: r.booker_name || null,
|
||||
players: players
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// migrate.js — Migreer data.json naar SQLite
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Laad de db.js module — we moeten even opletten dat we in de juiste dir staan
|
||||
process.env.DB_PATH = '/data/data.db';
|
||||
|
||||
// We importeren db.js met require, maar omdat we het nog niet in de container hebben
|
||||
// zetten we de module in de juiste dir
|
||||
console.log('📦 Data migratie: JSON → SQLite');
|
||||
console.log('────────────────────────────────\n');
|
||||
|
||||
const DATA_PATH = process.env.DATA_PATH || '/data/data.json';
|
||||
|
||||
if (!fs.existsSync(DATA_PATH)) {
|
||||
console.log('❌ Geen data.json gevonden op', DATA_PATH);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8'));
|
||||
console.log(`📖 Ingelezen: ${data.players ? data.players.length : 0} spelers, ${data.matches ? data.matches.length : 0} matches, ${data.settings ? Object.keys(data.settings).length : 0} settings`);
|
||||
|
||||
// Laad db.js — gebruik een relatief pad zodat we ook via node /app/api/migrate.js kunnen draaien
|
||||
const db = require('./db.cjs');
|
||||
|
||||
// ─── Players migreren ───
|
||||
if (Array.isArray(data.players)) {
|
||||
console.log(`\n👥 Spelers (${data.players.length}):`);
|
||||
for (const p of data.players) {
|
||||
try {
|
||||
const existing = db.getPlayerByTelegram(p.telegram_id);
|
||||
if (existing) {
|
||||
console.log(` ⏭️ ${p.name} — bestaat al (telegram_id ${p.telegram_id})`);
|
||||
continue;
|
||||
}
|
||||
db.addPlayer(p);
|
||||
console.log(` ✅ ${p.name} — level ${p.level}, ${p.position} (PIN: ${p.pin})`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ ${p.name} — FOUT: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Matches migreren ───
|
||||
if (Array.isArray(data.matches)) {
|
||||
console.log(`\n🎾 Matches (${data.matches.length}):`);
|
||||
for (const m of data.matches) {
|
||||
try {
|
||||
// Check of match al bestaat
|
||||
const existing = db.getMatch(m.id);
|
||||
if (existing) {
|
||||
console.log(` ⏭️ Match ${m.id} — bestaat al`);
|
||||
continue;
|
||||
}
|
||||
db.addMatch(m);
|
||||
console.log(` ✅ Match ${m.id} — status: ${m.status}, spelers: ${Array.isArray(m.players) ? m.players.length : 0}`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ Match ${m.id} — FOUT: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Settings migreren ───
|
||||
if (data.settings && typeof data.settings === 'object') {
|
||||
console.log(`\n⚙️ Settings (${Object.keys(data.settings).length}):`);
|
||||
for (const [key, value] of Object.entries(data.settings)) {
|
||||
db.setSetting(key, value);
|
||||
console.log(` ✅ ${key} = ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Sessions migreren ───
|
||||
if (Array.isArray(data.sessions)) {
|
||||
console.log(`\n🔑 Sessions (${data.sessions.length}):`);
|
||||
for (const s of data.sessions) {
|
||||
try {
|
||||
db.setSession(s.id, s.player_id || s.playerId, s.data || {});
|
||||
console.log(` ✅ Session ${s.id}`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ Session ${s.id} — FOUT: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Verificatie ───
|
||||
console.log('\n📊 Verificatie:');
|
||||
const players = db.getPlayers();
|
||||
const matches = db.getMatches();
|
||||
console.log(` 👥 Spelers in DB: ${players.length}`);
|
||||
console.log(` 🎾 Matches in DB: ${matches.length}`);
|
||||
console.log(` ⚙️ Settings in DB:`, JSON.stringify(db.getSettings()));
|
||||
|
||||
// Toon spelers
|
||||
for (const p of players) {
|
||||
const days = p.availability && !p.availability.isSet
|
||||
? Object.keys(p.availability).filter(k => k !== 'isSet' && k !== 'days').length
|
||||
: 0;
|
||||
console.log(` 📍 ${p.name}: level ${p.level}, ${days} beschikbare dagen`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Migratie voltooid!');
|
||||
console.log(`📁 data.json: ${DATA_PATH}`);
|
||||
console.log(`🗄️ SQLite: ${process.env.DB_PATH}`);
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "padel-planner",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node api/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"express": "^4.21.0",
|
||||
"playwright": "^1.60.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
// Peakz Padel beschikbaarheid scraper
|
||||
// Aanroep: node padel-peakz.mjs [datum] [locatie]
|
||||
// Voorbeeld: node padel-peakz.mjs 2026-05-24 Atoomweg
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const LOCATIONS = {
|
||||
'Atoomweg': '6637dd8e-cd4b-4fee-af49-196f6828b7dc',
|
||||
'Groningen': '6637dd8e-cd4b-4fee-af49-196f6828b7dc',
|
||||
};
|
||||
const RESERVATION_TYPE = '6';
|
||||
const PLAYING_TIME = '90';
|
||||
|
||||
async function getAvailability(dateStr, locationName) {
|
||||
const locationId = LOCATIONS[locationName] || LOCATIONS['Atoomweg'];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
const url = `https://www.peakzpadel.nl/reserveren/court-booking/reservation?daypart=---&date=${dateStr}&location=${encodeURIComponent(locationName)}&playingTimes=${PLAYING_TIME}&courtTypeIds=13`;
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const slots = await page.evaluate(() => {
|
||||
const buttons = document.querySelectorAll('button');
|
||||
const results = [];
|
||||
for (const btn of buttons) {
|
||||
const text = btn.textContent.trim();
|
||||
if (text.match(/^\d{2}:\d{2}/)) {
|
||||
const price = text.match(/€\s*([\d,]+)/);
|
||||
results.push({
|
||||
time: text.split(' ')[0],
|
||||
price: price ? price[1] : null,
|
||||
available: !btn.disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
return {
|
||||
date: dateStr,
|
||||
location: locationName,
|
||||
slots,
|
||||
available: slots.filter(s => s.available),
|
||||
unavailable: slots.filter(s => !s.available)
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI mode
|
||||
const dateArg = process.argv[2] || new Date().toISOString().split('T')[0];
|
||||
const locationArg = process.argv[3] || 'Atoomweg';
|
||||
|
||||
getAvailability(dateArg, locationArg).then(result => {
|
||||
if (result.error) {
|
||||
console.error('Fout:', result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Output als JSON voor machine-readable
|
||||
if (process.argv.includes('--json')) {
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Human readable
|
||||
const dayNames = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'];
|
||||
const d = new Date(result.date);
|
||||
const dayLabel = dayNames[d.getDay()];
|
||||
const dateLabel = `${dayLabel} ${d.getDate()}/${d.getMonth()+1}`;
|
||||
|
||||
console.log(`\n🏓 Peakz Padel — ${result.location}`);
|
||||
console.log(`📅 ${dateLabel}`);
|
||||
console.log(`🟢 ${result.available.length} banen vrij`);
|
||||
console.log(`🔴 ${result.unavailable.length} banen bezet`);
|
||||
console.log('');
|
||||
|
||||
if (result.available.length > 0) {
|
||||
console.log('Vrije tijden:');
|
||||
for (const s of result.available) {
|
||||
const price = s.price ? ` — €${s.price}` : '';
|
||||
console.log(` 🟢 ${s.time}${price}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -363,42 +363,6 @@ app.delete("/api/matches/:id", (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── TEAM INDELING (voorgesteld) ───
|
||||
app.post("/api/matches/:id/teams", (req, res) => {
|
||||
let m = db.getMatch(String(req.params.id));
|
||||
if (!m) return res.status(404).json({ error: "match not found" });
|
||||
const { team1, team2 } = req.body;
|
||||
if (!Array.isArray(team1) || !Array.isArray(team2))
|
||||
return res.status(400).json({ error: "team1 and team2 must be arrays" });
|
||||
const allIds = [...team1, ...team2];
|
||||
if (allIds.length !== 4)
|
||||
return res.status(400).json({ error: "expected 4 players total" });
|
||||
db.updateMatch(String(req.params.id), { proposed_teams: { team1, team2 } });
|
||||
m = db.getMatch(String(req.params.id));
|
||||
const playerIds = (m.players || []).map(p => p.id);
|
||||
res.json({ ...m, players: playerIds, players_array: m.players });
|
||||
});
|
||||
|
||||
app.get("/api/matches/:id/teams", (req, res) => {
|
||||
const m = db.getMatch(String(req.params.id));
|
||||
if (!m) return res.status(404).json({ error: "match not found" });
|
||||
const teams = m.proposed_teams || null;
|
||||
res.json({ teams, match: { id: m.id, date: m.date, start: m.start, status: m.status, match_type: m.match_type || 'friendly' } });
|
||||
});
|
||||
|
||||
// ─── MATCH TYPE (ranked/friendly) ───
|
||||
app.post("/api/matches/:id/type", (req, res) => {
|
||||
let m = db.getMatch(String(req.params.id));
|
||||
if (!m) return res.status(404).json({ error: "match not found" });
|
||||
const { match_type } = req.body;
|
||||
if (match_type !== 'ranked' && match_type !== 'friendly')
|
||||
return res.status(400).json({ error: "match_type must be 'ranked' or 'friendly'" });
|
||||
db.updateMatch(String(req.params.id), { match_type });
|
||||
m = db.getMatch(String(req.params.id));
|
||||
const playerIds = (m.players || []).map(p => p.id);
|
||||
res.json({ ...m, players: playerIds, players_array: m.players });
|
||||
});
|
||||
|
||||
// ─── SCORES ───
|
||||
app.post("/api/matches/:id/score", (req, res) => {
|
||||
let m = db.getMatch(String(req.params.id));
|
||||
@@ -439,8 +403,6 @@ app.post("/api/matches/:id/verify", (req, res) => {
|
||||
score.confirmed_at = new Date().toISOString();
|
||||
|
||||
// Update player wins/losses (idempotent via player_match_stats tabel)
|
||||
// Alleen voor ranked matches
|
||||
if (m.match_type === 'ranked') {
|
||||
for (const pid of score.team1) {
|
||||
const p = db.getPlayer(String(pid));
|
||||
if (p && !db.hasMatchStat(String(pid), m.id)) {
|
||||
@@ -458,7 +420,6 @@ app.post("/api/matches/:id/verify", (req, res) => {
|
||||
} else {
|
||||
score.status = hasOpponentConfirm ? "confirmed" : "partial";
|
||||
}
|
||||
}
|
||||
|
||||
db.updateMatch(String(req.params.id), { score });
|
||||
m = db.getMatch(String(req.params.id));
|
||||
|
||||
Reference in New Issue
Block a user