- WebAuthn: register/auth options, device management - PWA widget page + manifest shortcuts - Group schedule endpoint (timezones + locations) - UI #3-#6: compact headers on tasks/calendar/projects/goals - UI #9: mobile responsive top bars - webauthn_credentials table Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
5.7 KiB
JavaScript
142 lines
5.7 KiB
JavaScript
// Task Team — Groups CRUD — 2026-03-29
|
|
async function groupRoutes(app) {
|
|
app.get('/groups', async (req) => {
|
|
const lang = req.query.lang;
|
|
const { rows } = await app.db.query(
|
|
'SELECT * FROM task_groups ORDER BY order_index ASC'
|
|
);
|
|
const data = rows.map(row => {
|
|
if (lang && row.names && row.names[lang]) {
|
|
return { ...row, name: row.names[lang] };
|
|
}
|
|
return row;
|
|
});
|
|
return { data };
|
|
});
|
|
|
|
app.get('/groups/:id', async (req) => {
|
|
const lang = req.query.lang;
|
|
const { rows } = await app.db.query('SELECT * FROM task_groups WHERE id = $1', [req.params.id]);
|
|
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
|
const row = rows[0];
|
|
if (lang && row.names && row.names[lang]) {
|
|
row.name = row.names[lang];
|
|
}
|
|
return { data: row };
|
|
});
|
|
|
|
app.post('/groups', async (req) => {
|
|
const { name, color, icon, order_index, time_zones, user_id } = req.body;
|
|
const { rows } = await app.db.query(
|
|
'INSERT INTO task_groups (user_id, name, color, icon, order_index, time_zones) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *',
|
|
[user_id, name, color, icon || '', order_index || 0, JSON.stringify(time_zones || [])]
|
|
);
|
|
return { data: rows[0] };
|
|
});
|
|
|
|
app.put('/groups/:id', async (req) => {
|
|
const { name, color, icon, order_index, time_zones, locations } = req.body;
|
|
const { rows } = await app.db.query(
|
|
`UPDATE task_groups SET name=COALESCE($1,name), color=COALESCE($2,color), icon=COALESCE($3,icon),
|
|
order_index=COALESCE($4,order_index), time_zones=COALESCE($5,time_zones),
|
|
locations=COALESCE($6,locations), updated_at=NOW()
|
|
WHERE id=$7 RETURNING *`,
|
|
[name, color, icon, order_index,
|
|
time_zones ? JSON.stringify(time_zones) : null,
|
|
locations ? JSON.stringify(locations) : null,
|
|
req.params.id]
|
|
);
|
|
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
|
return { data: rows[0] };
|
|
});
|
|
|
|
app.delete('/groups/:id', async (req) => {
|
|
const { rowCount } = await app.db.query('DELETE FROM task_groups WHERE id = $1', [req.params.id]);
|
|
if (!rowCount) throw { statusCode: 404, message: 'Group not found' };
|
|
return { status: 'deleted' };
|
|
});
|
|
|
|
app.put('/groups/reorder', async (req) => {
|
|
const { order } = req.body; // [{id, order_index}]
|
|
const client = await app.db.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
for (const item of order) {
|
|
await client.query('UPDATE task_groups SET order_index=$1, updated_at=NOW() WHERE id=$2', [item.order_index, item.id]);
|
|
}
|
|
await client.query('COMMIT');
|
|
} catch (e) {
|
|
await client.query('ROLLBACK');
|
|
throw e;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
return { status: 'ok' };
|
|
});
|
|
|
|
// Update time zones for a group
|
|
app.put('/groups/:id/timezones', async (req) => {
|
|
const { time_zones } = req.body;
|
|
if (!Array.isArray(time_zones)) {
|
|
throw { statusCode: 400, message: 'time_zones must be an array of [{days, from, to}]' };
|
|
}
|
|
for (const tz of time_zones) {
|
|
if (!Array.isArray(tz.days) || !tz.from || !tz.to) {
|
|
throw { statusCode: 400, message: 'Each timezone must have days (array), from (HH:MM), to (HH:MM)' };
|
|
}
|
|
}
|
|
const { rows } = await app.db.query(
|
|
'UPDATE task_groups SET time_zones = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
|
[JSON.stringify(time_zones), req.params.id]
|
|
);
|
|
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
|
return { data: rows[0] };
|
|
});
|
|
|
|
// Update GPS locations for a group
|
|
app.put('/groups/:id/locations', async (req) => {
|
|
const { locations } = req.body;
|
|
if (!Array.isArray(locations)) {
|
|
throw { statusCode: 400, message: 'locations must be an array of [{name, lat, lng, radius_m}]' };
|
|
}
|
|
for (const loc of locations) {
|
|
if (!loc.name || loc.lat === undefined || loc.lng === undefined) {
|
|
throw { statusCode: 400, message: 'Each location must have name, lat, lng' };
|
|
}
|
|
if (typeof loc.lat !== 'number' || typeof loc.lng !== 'number') {
|
|
throw { statusCode: 400, message: 'lat and lng must be numbers' };
|
|
}
|
|
}
|
|
const { rows } = await app.db.query(
|
|
'UPDATE task_groups SET locations = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
|
[JSON.stringify(locations), req.params.id]
|
|
);
|
|
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
|
return { data: rows[0] };
|
|
});
|
|
|
|
// Combined schedule: time zones + locations for a group
|
|
app.get('/groups/:id/schedule', async (req) => {
|
|
const { rows } = await app.db.query(
|
|
'SELECT id, name, time_zones, locations FROM task_groups WHERE id = $1',
|
|
[req.params.id]
|
|
);
|
|
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
|
const g = rows[0];
|
|
return {
|
|
data: {
|
|
group_id: g.id,
|
|
group_name: g.name,
|
|
time_zones: g.time_zones || [],
|
|
locations: g.locations || [],
|
|
summary: {
|
|
tz_count: (g.time_zones || []).length,
|
|
loc_count: (g.locations || []).length
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
module.exports = groupRoutes;
|