mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: add calendar to app
This commit is contained in:
parent
b17715eaff
commit
08717c24bc
29 changed files with 1478 additions and 313 deletions
125
src/repositories/calendarRepo.js
Normal file
125
src/repositories/calendarRepo.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
const pool = require('../db/pool');
|
||||
const { normalizeISO, slotDateParis, dayOfWeekParis } = require('../lib/calendarDates');
|
||||
|
||||
const COLOR_PALETTE = [
|
||||
'#4285f4', '#ea4335', '#fbbc05', '#34a853',
|
||||
'#a142f4', '#24c1e0', '#ff6d00', '#795548',
|
||||
];
|
||||
|
||||
async function getPeople() {
|
||||
const { rows } = await pool.query('SELECT id, name, color FROM calendar_people ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Always called from within the ingestSlots transaction below (needs the
|
||||
// same client so the color-index count and the insert see a consistent view).
|
||||
async function upsertPerson(client, name) {
|
||||
const existing = await client.query('SELECT id FROM calendar_people WHERE name = $1', [name]);
|
||||
if (existing.rows[0]) return existing.rows[0].id;
|
||||
|
||||
const { rows: countRows } = await client.query('SELECT COUNT(*)::int AS count FROM calendar_people');
|
||||
const color = COLOR_PALETTE[countRows[0].count % COLOR_PALETTE.length];
|
||||
|
||||
const { rows } = await client.query(
|
||||
`INSERT INTO calendar_people (name, color) VALUES ($1, $2)
|
||||
ON CONFLICT (name) DO UPDATE SET name = excluded.name
|
||||
RETURNING id`,
|
||||
[name, color]
|
||||
);
|
||||
return rows[0].id;
|
||||
}
|
||||
|
||||
async function ingestSlots(slots) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
for (const slot of slots) {
|
||||
const lower = normalizeISO(slot.lower);
|
||||
const upper = normalizeISO(slot.upper);
|
||||
const slotDate = slotDateParis(slot.lower);
|
||||
const dayOfWeek = dayOfWeekParis(slot.lower);
|
||||
|
||||
const { rows: slotRows } = await client.query(
|
||||
`INSERT INTO calendar_slots (lower, upper, slot_date, day_of_week)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (lower, upper) DO UPDATE SET
|
||||
slot_date = excluded.slot_date,
|
||||
day_of_week = excluded.day_of_week
|
||||
RETURNING id`,
|
||||
[lower, upper, slotDate, dayOfWeek]
|
||||
);
|
||||
const slotId = slotRows[0].id;
|
||||
|
||||
// people may arrive as a JSON string from n8n's Set-node serialization.
|
||||
const people = typeof slot.people === 'string' ? JSON.parse(slot.people) : slot.people || [];
|
||||
|
||||
for (const person of people) {
|
||||
if (!person || !person.name) continue;
|
||||
const personId = await upsertPerson(client, person.name);
|
||||
await client.query(
|
||||
`INSERT INTO calendar_availability (slot_id, person_id, is_available, checked_at)
|
||||
VALUES ($1, $2, $3, now())
|
||||
ON CONFLICT (slot_id, person_id) DO UPDATE SET
|
||||
is_available = excluded.is_available,
|
||||
checked_at = excluded.checked_at`,
|
||||
[slotId, personId, !!person.available]
|
||||
);
|
||||
}
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function getSlots({ minPeople = 1, personIds = null, weeks = 3 } = {}) {
|
||||
const cappedWeeks = Math.min(weeks || 3, 3);
|
||||
const now = new Date();
|
||||
const end = new Date(now);
|
||||
end.setDate(end.getDate() + cappedWeeks * 7);
|
||||
|
||||
const nowStr = now.toISOString().substring(0, 10);
|
||||
const endStr = end.toISOString().substring(0, 10);
|
||||
const hasPersonFilter = Array.isArray(personIds) && personIds.length > 0;
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT
|
||||
ts.id, ts.lower, ts.upper, ts.slot_date, ts.day_of_week,
|
||||
filtered.available_count, filtered.total_in_filter,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object('id', p.id, 'name', p.name, 'color', p.color,
|
||||
'is_available', sa2.is_available)
|
||||
ORDER BY p.id
|
||||
)
|
||||
FROM calendar_availability sa2
|
||||
JOIN calendar_people p ON p.id = sa2.person_id
|
||||
WHERE sa2.slot_id = ts.id
|
||||
) AS people
|
||||
FROM calendar_slots ts
|
||||
JOIN (
|
||||
SELECT slot_id,
|
||||
COUNT(*) FILTER (WHERE is_available) AS available_count,
|
||||
COUNT(*) AS total_in_filter
|
||||
FROM calendar_availability
|
||||
WHERE ($3::int[] IS NULL OR person_id = ANY($3::int[]))
|
||||
GROUP BY slot_id
|
||||
) filtered ON filtered.slot_id = ts.id
|
||||
WHERE ts.slot_date >= $1 AND ts.slot_date <= $2
|
||||
AND filtered.available_count >= $4
|
||||
ORDER BY ts.lower`,
|
||||
[nowStr, endStr, hasPersonFilter ? personIds : null, minPeople]
|
||||
);
|
||||
|
||||
return rows.map((row) => ({ ...row, people: row.people || [] }));
|
||||
}
|
||||
|
||||
async function getLastChecked() {
|
||||
const { rows } = await pool.query('SELECT MAX(checked_at) AS ts FROM calendar_availability');
|
||||
return rows[0].ts;
|
||||
}
|
||||
|
||||
module.exports = { getPeople, ingestSlots, getSlots, getLastChecked };
|
||||
|
|
@ -2,7 +2,7 @@ const pool = require('../db/pool');
|
|||
|
||||
async function findAll() {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.description, sg.status, sg.promoted_song_id,
|
||||
SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.spotify_url, sg.description, sg.status, sg.promoted_song_id,
|
||||
sg.created_at, sg.suggested_by, u.name AS suggested_by_name,
|
||||
COUNT(*) FILTER (WHERE v.vote = 'approve')::int AS approve_count,
|
||||
COUNT(*) FILTER (WHERE v.vote = 'reject')::int AS reject_count
|
||||
|
|
@ -17,7 +17,7 @@ async function findAll() {
|
|||
|
||||
async function findById(id) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.description, sg.status, sg.promoted_song_id,
|
||||
`SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.spotify_url, sg.description, sg.status, sg.promoted_song_id,
|
||||
sg.created_at, sg.suggested_by, u.name AS suggested_by_name
|
||||
FROM suggestions sg
|
||||
JOIN users u ON u.id = sg.suggested_by
|
||||
|
|
@ -39,12 +39,12 @@ async function findVotes(suggestionId) {
|
|||
return rows;
|
||||
}
|
||||
|
||||
async function create({ title, artist, youtubeUrl, description, suggestedBy }) {
|
||||
async function create({ title, artist, youtubeUrl, spotifyUrl, description, suggestedBy }) {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO suggestions (title, artist, youtube_url, description, suggested_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, title, artist, youtube_url, description, status, promoted_song_id, created_at, suggested_by`,
|
||||
[title, artist || null, youtubeUrl, description || null, suggestedBy]
|
||||
`INSERT INTO suggestions (title, artist, youtube_url, spotify_url, description, suggested_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, title, artist, youtube_url, spotify_url, description, status, promoted_song_id, created_at, suggested_by`,
|
||||
[title, artist || null, youtubeUrl, spotifyUrl || null, description || null, suggestedBy]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ async function create({ title, artist, youtubeUrl, description, suggestedBy }) {
|
|||
async function updateStatus(id, status) {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE suggestions SET status = $2, updated_at = now() WHERE id = $1
|
||||
RETURNING id, title, artist, youtube_url, description, status, promoted_song_id, created_at, suggested_by`,
|
||||
RETURNING id, title, artist, youtube_url, spotify_url, description, status, promoted_song_id, created_at, suggested_by`,
|
||||
[id, status]
|
||||
);
|
||||
return rows[0] || null;
|
||||
|
|
@ -94,10 +94,17 @@ async function promoteToSong(suggestionId, addedBy) {
|
|||
return null;
|
||||
}
|
||||
const { rows: songRows } = await client.query(
|
||||
`INSERT INTO songs (title, artist, notes, youtube_url, added_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`INSERT INTO songs (title, artist, notes, youtube_url, spotify_url, added_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, title, artist, notes, youtube_url, spotify_url, added_by, created_at, updated_at`,
|
||||
[suggestion.title, suggestion.artist || suggestion.title, suggestion.description, suggestion.youtube_url, addedBy]
|
||||
[
|
||||
suggestion.title,
|
||||
suggestion.artist || suggestion.title,
|
||||
suggestion.description,
|
||||
suggestion.youtube_url,
|
||||
suggestion.spotify_url,
|
||||
addedBy,
|
||||
]
|
||||
);
|
||||
const song = songRows[0];
|
||||
await client.query(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue