Compare commits
2 Commits
7051681531
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 174fc6872e | |||
| 9c48217c36 |
@@ -270,13 +270,17 @@ app.patch('/api/activities/:id', (req, res) => {
|
|||||||
const emoji = req.body.emoji ?? existing.emoji;
|
const emoji = req.body.emoji ?? existing.emoji;
|
||||||
const image_url = req.body.image_url !== undefined ? req.body.image_url : existing.image_url;
|
const image_url = req.body.image_url !== undefined ? req.body.image_url : existing.image_url;
|
||||||
const cost_shells = req.body.cost_shells !== undefined ? Number(req.body.cost_shells) : existing.cost_shells;
|
const cost_shells = req.body.cost_shells !== undefined ? Number(req.body.cost_shells) : existing.cost_shells;
|
||||||
const status = req.body.status ?? existing.status;
|
const current_shells = req.body.current_shells !== undefined ? Math.max(0, Number(req.body.current_shells)) : existing.current_shells;
|
||||||
|
let status = req.body.status ?? existing.status;
|
||||||
|
if (current_shells >= cost_shells && status === 'ACTIVE') {
|
||||||
|
status = 'UNLOCKED';
|
||||||
|
}
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE activities
|
UPDATE activities
|
||||||
SET title = ?, description = ?, emoji = ?, image_url = ?, cost_shells = ?, status = ?
|
SET title = ?, description = ?, emoji = ?, image_url = ?, cost_shells = ?, current_shells = ?, status = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(title, description, emoji, image_url, cost_shells, status, id);
|
`).run(title, description, emoji, image_url, cost_shells, current_shells, status, id);
|
||||||
|
|
||||||
res.json({ message: 'Activité mise à jour.' });
|
res.json({ message: 'Activité mise à jour.' });
|
||||||
});
|
});
|
||||||
@@ -290,6 +294,71 @@ app.delete('/api/activities/:id', (req, res) => {
|
|||||||
res.json({ message: 'Activité supprimée.' });
|
res.json({ message: 'Activité supprimée.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 6b. POST /api/activities/:id/direct-contribute (Attribution directe de coquillages par les parents pour un enfant)
|
||||||
|
app.post('/api/activities/:id/direct-contribute', (req, res) => {
|
||||||
|
const activityId = Number(req.params.id);
|
||||||
|
const activity = db.prepare('SELECT * FROM activities WHERE id = ?').get(activityId);
|
||||||
|
if (!activity) return res.status(404).json({ error: 'Activité introuvable.' });
|
||||||
|
|
||||||
|
const { child_id, shells, source_type = 'PHYSICAL' } = req.body;
|
||||||
|
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(Number(child_id));
|
||||||
|
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
|
||||||
|
|
||||||
|
const count = Number(shells);
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
return res.status(400).json({ error: 'Veuillez saisir un nombre de coquillages supérieur à 0.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVirtual = source_type === 'VIRTUAL';
|
||||||
|
if (isVirtual && child.available_shells < count) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Solde virtuel insuffisant : ${child.name} n'a que ${child.available_shells} coquillages disponibles.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Déduire si c'est virtuel
|
||||||
|
if (isVirtual) {
|
||||||
|
db.prepare('UPDATE children SET available_shells = available_shells - ? WHERE id = ?').run(count, child.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créditer l'activité
|
||||||
|
const newTotal = activity.current_shells + count;
|
||||||
|
const newStatus = newTotal >= activity.cost_shells ? 'UNLOCKED' : activity.status;
|
||||||
|
db.prepare('UPDATE activities SET current_shells = ?, status = ? WHERE id = ?').run(newTotal, newStatus, activity.id);
|
||||||
|
|
||||||
|
// Enregistrer la contribution
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO activity_contributions (activity_id, child_id, shells, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(activity.id, child.id, count, now);
|
||||||
|
|
||||||
|
// Résoudre d'éventuelles requêtes Alexa PENDING pour cet enfant sur cette activité afin de déverrouiller son bouton
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE pending_requests
|
||||||
|
SET status = 'APPROVED', resolved_at = ?
|
||||||
|
WHERE activity_id = ? AND child_id = ? AND status = 'PENDING'
|
||||||
|
`).run(now, activity.id, child.id);
|
||||||
|
|
||||||
|
// Enregistrer la transaction
|
||||||
|
const sourceLabel = isVirtual ? 'déduits du compte virtuel' : 'coquillages physiques remis';
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'CONTRIBUTION', ?, 0, ?, ?)
|
||||||
|
`).run(
|
||||||
|
child.id,
|
||||||
|
isVirtual ? -count : 0,
|
||||||
|
`${child.name} a mis ${count} 🐚 (${sourceLabel}) pour "${activity.title}" (${newTotal}/${activity.cost_shells})`,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `${count} 🐚 attribués avec succès à "${activity.title}" pour ${child.name} !`,
|
||||||
|
activity: getEnrichedActivities().find((a) => a.id === activityId)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 7. POST /api/requests
|
// 7. POST /api/requests
|
||||||
app.post('/api/requests', (req, res) => {
|
app.post('/api/requests', (req, res) => {
|
||||||
const {
|
const {
|
||||||
@@ -701,6 +770,125 @@ app.post('/api/children/:id/adjust', (req, res) => {
|
|||||||
res.json({ message: 'Compte mis à jour.', child: getEnrichedChildren().find((c) => c.id === id) });
|
res.json({ message: 'Compte mis à jour.', child: getEnrichedChildren().find((c) => c.id === id) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 13b. PUT /api/children/:id (Édition directe des soldes de l'enfant par les parents)
|
||||||
|
app.put('/api/children/:id', (req, res) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(id);
|
||||||
|
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
|
||||||
|
|
||||||
|
const { available_shells, locked_shells, euros_balance } = req.body;
|
||||||
|
const newAvailable = available_shells !== undefined ? Math.max(0, Number(available_shells)) : child.available_shells;
|
||||||
|
const newLocked = locked_shells !== undefined ? Math.max(0, Number(locked_shells)) : child.locked_shells;
|
||||||
|
const newEuros = euros_balance !== undefined ? Math.max(0, Number(euros_balance)) : child.euros_balance;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE children
|
||||||
|
SET available_shells = ?, locked_shells = ?, euros_balance = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(newAvailable, newLocked, newEuros, id);
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'ADMIN_EDIT', ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
id,
|
||||||
|
newAvailable - child.available_shells,
|
||||||
|
newEuros - child.euros_balance,
|
||||||
|
`Mise à jour des soldes de ${child.name} : ${newAvailable} 🐚 dispo, ${newLocked} 🔒 bloqués, ${newEuros.toFixed(2)} €`,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `Soldes de ${child.name} enregistrés avec succès !`,
|
||||||
|
child: getEnrichedChildren().find((c) => c.id === id)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 13c. POST /api/children/:id/transfer (Transfert réel <-> virtuel, blocage/déblocage rapide)
|
||||||
|
app.post('/api/children/:id/transfer', (req, res) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(id);
|
||||||
|
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
|
||||||
|
|
||||||
|
const { type, shells, reason = '' } = req.body;
|
||||||
|
const count = Number(shells);
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
return res.status(400).json({ error: 'Le nombre de coquillages doit être supérieur à 0.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'REAL_TO_AVAILABLE': {
|
||||||
|
// Coquillages physiques reçus -> crédités sur le compte disponible
|
||||||
|
db.prepare('UPDATE children SET available_shells = available_shells + ? WHERE id = ?').run(count, id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'TRANSFER_REAL_AVAILABLE', ?, 0, ?, ?)
|
||||||
|
`).run(id, count, `Dépôt réel : +${count} 🐚 physiques ajoutés au compte disponible de ${child.name} (${reason || 'Remis en main propre'})`, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REAL_TO_LOCKED': {
|
||||||
|
// Coquillages physiques reçus -> directement bloqués sur le livret (+intérêts)
|
||||||
|
db.prepare('UPDATE children SET locked_shells = locked_shells + ? WHERE id = ?').run(count, id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'TRANSFER_REAL_LOCKED', ?, 0, ?, ?)
|
||||||
|
`).run(id, count, `Dépôt réel bloqué : +${count} 🔒 bloqués pour ${child.name} (${reason || 'Livret épargne'})`, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'AVAILABLE_TO_REAL': {
|
||||||
|
// L'enfant reprend/utilise des coquillages réels -> débités du compte virtuel disponible
|
||||||
|
if (child.available_shells < count) {
|
||||||
|
return res.status(400).json({ error: `Solde insuffisant : ${child.name} n'a que ${child.available_shells} coquillages disponibles.` });
|
||||||
|
}
|
||||||
|
db.prepare('UPDATE children SET available_shells = available_shells - ? WHERE id = ?').run(count, id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'TRANSFER_AVAILABLE_REAL', ?, 0, ?, ?)
|
||||||
|
`).run(id, -count, `Retrait réel : -${count} 🐚 utilisés dans la vraie vie par ${child.name} (${reason || 'Utilisation réelle'})`, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'LOCK': {
|
||||||
|
// Transfert interne : disponible -> bloqué
|
||||||
|
if (child.available_shells < count) {
|
||||||
|
return res.status(400).json({ error: `Solde disponible insuffisant (${child.available_shells} 🐚 dispo).` });
|
||||||
|
}
|
||||||
|
db.prepare('UPDATE children SET available_shells = available_shells - ?, locked_shells = locked_shells + ? WHERE id = ?').run(count, count, id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'LOCK_VIRTUAL', 0, 0, ?, ?)
|
||||||
|
`).run(id, `${child.name} a bloqué ${count} 🐚 vers son livret épargne`, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UNLOCK': {
|
||||||
|
// Transfert interne : bloqué -> disponible
|
||||||
|
if (child.locked_shells < count) {
|
||||||
|
return res.status(400).json({ error: `Solde bloqué insuffisant (${child.locked_shells} 🔒 bloqués).` });
|
||||||
|
}
|
||||||
|
db.prepare('UPDATE children SET available_shells = available_shells + ?, locked_shells = locked_shells - ? WHERE id = ?').run(count, count, id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, 'UNLOCK_VIRTUAL', 0, 0, ?, ?)
|
||||||
|
`).run(id, `${count} 🐚 bloqués reversés sur le compte disponible de ${child.name}`, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return res.status(400).json({ error: `Type de transfert inconnu: ${type}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `Opération enregistrée pour ${child.name} !`,
|
||||||
|
child: getEnrichedChildren().find((c) => c.id === id)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 14. POST /api/alexa/voice-intent (Avec support Personalisation Voice ID, cartes illustrées & APL Echo Show)
|
// 14. POST /api/alexa/voice-intent (Avec support Personalisation Voice ID, cartes illustrées & APL Echo Show)
|
||||||
app.post('/api/alexa/voice-intent', (req, res) => {
|
app.post('/api/alexa/voice-intent', (req, res) => {
|
||||||
const { intent, slots = {}, personId = null, personGivenName = null } = req.body;
|
const { intent, slots = {}, personId = null, personGivenName = null } = req.body;
|
||||||
|
|||||||
1001
front/src/App.jsx
1001
front/src/App.jsx
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user