// @ts-check
/**
* @import {ExpressRequestAuthorized, ExpressResponse} from '../../types.js'
*/
import {query,transaction,UUID2hex,HEX2uuid,HEX2base64} from '@commtool/sql-query'
import {Templates} from '../../utils/compileTemplates.js'
import {getUID, isValidUID} from '../../utils/UUIDs.js'
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import {addUpdateList} from '../../server.ws.js'
import {isObjectAdmin} from '../../utils/authChecks.js'
import { errorLoggerUpdate} from '../../utils/requestLogger.js'
import {authorizeUser, updateAchievements} from './utilities.js'
import {renderObject} from '../../utils/renderTemplates.js'
import {isAdmin} from '../../utils/authChecks.js'
import { recreateJobsPerson } from '../job/utilities.js';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'
/**
* Updates an existing achievement in the database.
*
* @async
* @function updateAchievement
* @param {Object} object - Achievement object with updated data
* @param {string|Buffer} achievementUID - UUID (hex string or Buffer) of the achievement
* @param {string|Buffer} templateUID - UUID (hex string or Buffer) of the template
* @param {Object} session - User session data
* @param {Object} connection - Database connection for transaction
* @returns {Promise<void>}
*/
async function updateAchievement(object, achievementUID, templateUID, session, connection) {
await connection.query(
`UPDATE ObjectBase SET Title=?,SortName=?,dindex=?,Data=?, UIDuser=?
WHERE UID=?`,
[object.Title, object.SortIndex, object.dindex, JSON.stringify(object.Data), UUID2hex(session.user), object.UID]
);
await query(
`INSERT IGNORE INTO Links(UID, Type, UIDTarget, UIDuser) VALUES (?,'achievement',?,?)`,
[achievementUID, templateUID, UUID2hex(session.user)]
);
}
/**
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'
* Inserts a new achievement into the database.
*
* @async
* @function insertAchievement
* @param {Object} object - Achievement object to insert
* @param {string|Buffer} achievementUID - UUID (hex string or Buffer) of the achievement
* @param {string|Buffer} templateUID - UUID (hex string or Buffer) of the template
* @param {Object} session - User session data
* @param {string|Buffer} memberUID - UUID (hex string or Buffer) of the member
* @param {Object} connection - Database connection for transaction
* @returns {Promise<void>}
*/
async function insertAchievement(object, achievementUID, templateUID, session, memberUID, connection) {
await query(
`INSERT INTO ObjectBase (UID,Type,UIDuser,UIDBelongsTo,Title,SortName,dindex,Data)
VALUES (?,'achievement',?,?,?,?,?,?) ON DUPLICATE KEY UPDATE
Title=VALUE(Title),SortName=VALUE(SortName),dindex=VALUE(dindex),Data=VALUE(Data), UIDuser=VALUE(UIDuser)`,
[object.UID, UUID2hex(session.user), memberUID, object.Title, object.SortIndex, object.dindex, JSON.stringify(object.Data)],
{ connection: connection }
);
await connection.query(
`INSERT IGNORE INTO Links(UID, Type, UIDTarget, UIDuser) VALUES (?,'achievement',?,?)`,
[achievementUID, templateUID, UUID2hex(session.user)]
);
}
/**
* Handles the insertion or update of an achievement for a member.
*
* This function processes achievement data from the request, checks user authorization,
* validates the member and template, and either creates a new achievement or updates an existing one.
* It also handles achievement renewal logic and queues necessary updates.
*
* @async
* @function insertOrUpdateAchievement
* @param {Object} req - The request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.member - UUID of the member
* @param {string} req.params.template - UUID of the achievement template
* @param {Object} req.body - Request body
* @param {string} req.body.UID - Achievement UUID
* @param {number} [req.body.date] - Date of the achievement (timestamp)
* @param {string} [req.body.userAdded] - User who added the achievement
* @param {Object} req.session - User session data
* @param {string} req.session.user - Current user's UUID
* @param {string} req.session.root - Root UUID
* @param {Object} res - The response object
* @returns {Promise<void>} - Sends JSON response with success status and result
* @throws {Error} - Logs errors with errorLoggerUpdate
*/
export const insertOrUpdateAchievement = async (req, res) => {
try {
// Extract and prepare parameters
const timestamp = parseTimestampToSeconds(req.body.date);
const memberUID = UUID2hex(req.params.member);
const templateUID = UUID2hex(req.params.template);
const achievementUID = await getUID(req);
// Validate achievement template
const aTemplates = await query(
`SELECT Data FROM ObjectBase WHERE UID=? AND Type='achievementT'`,
[templateUID],
{ cast: ['json'] }
);
if (!aTemplates.length) {
return res.status(400).json({ success: false, message: 'Missing or unknown template' });
}
// Extract template data
const aTemplate = aTemplates[0];
const achievementData = aTemplate.Data;
// Check user authorization
const authorized = await authorizeUser(req.session.user, achievementData.authorized);
if (!isObjectAdmin(req, memberUID)) {
return res.status(403).json({
success: false,
message: 'User not authorized to change information for this person'
});
}
if (!authorized && !await isAdmin(req.session)) {
return res.status(403).json({
success: false,
message: 'User not authorized to add this achievement'
});
}
// Validate member exists
const baseMembers = await query(`SELECT Data FROM ObjectBase WHERE UID=?`, [memberUID]);
if (!baseMembers.length) {
return res.status(400).json({ success: false, message: 'Invalid base member UID' });
}
const members = await query(
`SELECT Data FROM Member WHERE UID=?`,
[memberUID],
{ cast: ['json'] }
);
if (!members.length) {
return res.status(400).json({ success: false, message: 'Invalid member UID' });
}
// Prepare object data
const member = members[0];
const personData = member.Data;
const template = Templates[req.session.root].achievement;
const userAdded = req.body.userAdded || req.session.user;
let date = req.body.date !== null && req.body.date !== undefined && req.body.date > 0
? req.body.date
: Date.now();
// Render the achievement object
const object = await renderObject(
template,
{
person: personData,
achievement: achievementData,
UID: req.body.UID,
date: date,
dateAdded: Date.now()/1000,
},
req
);
object.Data = {
renewal: achievementData.renewal,
date: date,
userAdded: userAdded
};
// Check for existing achievement
let achievement = null;
let treeQueue = true;
const achievements = await query(
`SELECT ObjectBase.UID, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom, ObjectBase.dindex
FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID)
WHERE ObjectBase.Type='achievement' AND ObjectBase.UIDBelongsTo=? AND Links.UIDTarget=?`,
[memberUID, templateUID]
);
// Handle existing achievement
if (achievements.length > 0) {
achievement = achievements[0];
// Use existing date if newer or if renewal isn't allowed
if (achievement.validFrom > date || !achievementData.renewal) {
date = achievement.validFrom;
}
treeQueue = false;
// Update existing achievement
object.UID = achievement.UID;
}
// Calculate expiration for renewable achievements
object.dindex = null;
if (achievementData.renewal) {
const sinceDays = (Date.now() - object.Data.date) / 86400000;
const expire = Math.floor(parseInt(achievementData.renewal) - sinceDays);
if (expire < 0) {
object.dindex = 1; // 1 means expired
}
if (achievement && achievement.dindex !== object.dindex) {
treeQueue = true;
}
}
// Validate UID format
if (!isValidUID(req.body.UID)) {
return res.status(400).json({ success: false, message: 'Invalid UID format in body.UID' });
}
// Database transaction
await transaction(async (connection) => {
if (achievement) {
await updateAchievement(
object,
achievementUID,
templateUID,
req.session,
connection,
);
} else {
await insertAchievement(
object,
achievementUID,
templateUID,
req.session,
memberUID,
connection
);
}
// Queue updates if needed (only when achievement is added or expiry update)
if (treeQueue) {
// Recreate jobs for the person when an achievement is added or updated
// This ensures job assignments are updated based on new qualification achievements
recreateJobsPerson(req,memberUID);
// Queue tree updates for list membership
queueAdd(
UUID2hex(req.session.root),
UUID2hex(req.session.user),
'listMember',
memberUID,
memberUID,
null,
memberUID,
timestamp
);
}
}, {
backDate: achievement ? Math.max(timestamp, achievement.validFrom) : timestamp
});
// Send success response
res.json({
success: true,
result: { ...object, UID: HEX2uuid(object.UID) }
});
// Update achievements and lists
updateAchievements(memberUID);
addUpdateList(memberUID);
} catch (e) {
res.status(500).json({ success: false, message: 'Internal server error' });
errorLoggerUpdate(e);
}
};