"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "usersRouter", {
    enumerable: true,
    get: function() {
        return usersRouter;
    }
});
const _express = /*#__PURE__*/ _interop_require_default(require("express"));
const _expressasynchandler = /*#__PURE__*/ _interop_require_default(require("express-async-handler"));
const _sequelize = require("sequelize");
const _dateTime = require("@tamanu/utils/dateTime");
const _lodash = require("lodash");
const _yup = /*#__PURE__*/ _interop_require_wildcard(require("yup"));
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _datefns = require("date-fns");
const _password = require("@tamanu/utils/password");
const _ability = require("@casl/ability");
const _zod = /*#__PURE__*/ _interop_require_default(require("zod"));
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
function _getRequireWildcardCache(nodeInterop) {
    if (typeof WeakMap !== "function") return null;
    var cacheBabelInterop = new WeakMap();
    var cacheNodeInterop = new WeakMap();
    return (_getRequireWildcardCache = function(nodeInterop) {
        return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
    })(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
    if (!nodeInterop && obj && obj.__esModule) {
        return obj;
    }
    if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
        return {
            default: obj
        };
    }
    var cache = _getRequireWildcardCache(nodeInterop);
    if (cache && cache.has(obj)) {
        return cache.get(obj);
    }
    var newObj = {
        __proto__: null
    };
    var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
    for(var key in obj){
        if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
            var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
            if (desc && (desc.get || desc.set)) {
                Object.defineProperty(newObj, key, desc);
            } else {
                newObj[key] = obj[key];
            }
        }
    }
    newObj.default = obj;
    if (cache) {
        cache.set(obj, newObj);
    }
    return newObj;
}
const usersRouter = _express.default.Router();
const createUserFilters = (filterParams, models)=>{
    const includeDeactivated = filterParams.includeDeactivated !== 'false';
    const filters = [
        // Text search filters
        filterParams.displayName && {
            [_sequelize.Op.or]: [
                {
                    displayName: {
                        [_sequelize.Op.iLike]: `%${filterParams.displayName}%`
                    }
                }
            ]
        },
        filterParams.displayId && {
            displayId: {
                [_sequelize.Op.iLike]: `%${filterParams.displayId}%`
            }
        },
        filterParams.email && {
            email: {
                [_sequelize.Op.iLike]: `%${filterParams.email}%`
            }
        },
        // Exact match filters
        filterParams.roleId && {
            role: filterParams.roleId
        },
        // Designation filter
        filterParams.designationId && {
            id: {
                [_sequelize.Op.in]: models.User.sequelize.literal(`(
          SELECT "user_id"
          FROM "user_designations"
          WHERE "designation_id" = ${models.User.sequelize.escape(filterParams.designationId)}
          AND "deleted_at" IS NULL
        )`)
            }
        },
        // Include deactivated users filter
        !includeDeactivated && {
            visibilityStatus: 'current'
        },
        // Exclude system user
        {
            id: {
                [_sequelize.Op.ne]: _constants.SYSTEM_USER_UUID
            }
        },
        // Exclude admin user
        {
            email: {
                [_sequelize.Op.ne]: _constants.ADMIN_USER_EMAIL
            }
        }
    ];
    return filters.filter((f)=>!!f);
};
usersRouter.get('/', (0, _expressasynchandler.default)(async (req, res)=>{
    const { store: { models: { User, UserDesignation, ReferenceData, Role } }, query: { order = 'ASC', orderBy = 'displayName', rowsPerPage, page, ...filterParams } } = req;
    req.checkPermission('list', 'User');
    // Create where clause from filters
    const filters = createUserFilters(filterParams, req.store.models);
    const whereClause = filters.length > 0 ? {
        [_sequelize.Op.and]: filters
    } : {};
    const userInclude = [
        'facilities',
        {
            model: UserDesignation,
            as: 'designations',
            include: {
                model: ReferenceData,
                as: 'referenceData'
            }
        }
    ];
    // Get total count for pagination
    const count = await User.count({
        where: whereClause,
        include: userInclude,
        distinct: true
    });
    let orderClause;
    const upperOrder = order.toUpperCase();
    switch(orderBy){
        case 'roleName':
            orderClause = [
                [
                    User.sequelize.literal(`(SELECT "name" FROM ${Role.getTableName()} WHERE ${Role.getTableName()}."id" = "User"."role")`),
                    upperOrder
                ]
            ];
            break;
        case 'designations':
            orderClause = [
                [
                    User.sequelize.literal(`(SELECT MIN(ref."name") FROM ${UserDesignation.getTableName()} ud
                LEFT JOIN ${ReferenceData.getTableName()} ref ON ud."designation_id" = ref."id"
                WHERE ud."user_id" = "User"."id" AND ud."deleted_at" IS NULL AND ref."deleted_at" IS NULL
                GROUP BY ud."user_id")
              `),
                    upperOrder
                ]
            ];
            break;
        case 'displayName':
        case 'email':
        case 'phoneNumber':
            orderClause = [
                [
                    orderBy,
                    upperOrder
                ]
            ];
            break;
        default:
            throw new _errors.ValidationError(`Invalid orderBy value: ${orderBy}`);
    }
    const users = await User.findAll({
        where: whereClause,
        include: userInclude,
        order: orderClause,
        limit: rowsPerPage,
        offset: page && rowsPerPage ? page * rowsPerPage : undefined
    });
    // Get role names for each user
    const roleIds = [
        ...new Set(users.map((user)=>user.role))
    ];
    const roles = await Role.findAll({
        where: {
            id: roleIds
        }
    });
    const roleMap = new Map(roles.map((role)=>[
            role.id,
            role.name
        ]));
    res.send({
        count,
        data: await Promise.all(users.map(async (user)=>{
            const allowedFacilities = await user.allowedFacilityIds();
            const obj = user.get({
                plain: true
            });
            const designations = user.designations || [];
            const roleName = roleMap.get(user.role) || null;
            return {
                ...(0, _lodash.pick)(obj, [
                    'id',
                    'displayName',
                    'displayId',
                    'email',
                    'phoneNumber',
                    'role',
                    'visibilityStatus',
                    'facilities'
                ]),
                roleName,
                allowedFacilities,
                designations
            };
        }))
    });
}));
const CREATE_VALIDATION = _yup.object().shape({
    displayName: _yup.string().trim().required(),
    displayId: _yup.string().trim().nullable().optional(),
    role: _yup.string().required(),
    phoneNumber: _yup.string().trim().nullable().optional(),
    email: _yup.string().trim().email().required(),
    designations: _yup.array().of(_yup.string()).nullable().optional(),
    password: _yup.string().required(),
    allowedFacilityIds: _yup.array().of(_yup.string()).nullable().optional()
}).test('password-is-not-hashed', 'Password must not be hashed', function(value) {
    return !(0, _password.isBcryptHash)(value.password);
}).noUnknown();
usersRouter.post('/', (0, _expressasynchandler.default)(async (req, res)=>{
    const { store: { models: { Role, User, ReferenceData, UserDesignation, UserFacility, Facility } }, db } = req;
    req.checkPermission('create', 'User');
    const fields = await CREATE_VALIDATION.validate(req.body);
    const role = await Role.findByPk(fields.role);
    if (!role) {
        throw new _errors.NotFoundError('Role not found');
    }
    const existingUserWithSameEmail = await User.findOne({
        where: {
            email: fields.email
        }
    });
    if (existingUserWithSameEmail) {
        throw new _errors.DatabaseDuplicateError('Email must be unique across all users');
    }
    const existingUserWithSameDisplayName = await User.findOne({
        where: {
            displayName: {
                [_sequelize.Op.iLike]: fields.displayName
            }
        }
    });
    if (existingUserWithSameDisplayName) {
        throw new _errors.DatabaseDuplicateError('Display name must be unique across all users');
    }
    if (fields.designations && fields.designations.length > 0) {
        // Check if all designation IDs exist and are of type 'designation'
        const existingDesignations = await ReferenceData.findAll({
            where: {
                id: {
                    [_sequelize.Op.in]: fields.designations
                },
                type: _constants.REFERENCE_TYPES.DESIGNATION,
                visibilityStatus: _constants.VISIBILITY_STATUSES.CURRENT
            },
            attributes: [
                'id'
            ]
        });
        const existingDesignationIds = existingDesignations.map((d)=>d.id);
        const invalidDesignationIds = fields.designations.filter((id)=>!existingDesignationIds.includes(id));
        if (invalidDesignationIds.length > 0) {
            throw new _errors.ValidationError(`Invalid designation IDs: ${invalidDesignationIds.join(', ')}`);
        }
    }
    if (fields.allowedFacilityIds && fields.allowedFacilityIds.length > 0) {
        const existingFacilities = await Facility.findAll({
            where: {
                id: {
                    [_sequelize.Op.in]: fields.allowedFacilityIds
                },
                visibilityStatus: _constants.VISIBILITY_STATUSES.CURRENT
            },
            attributes: [
                'id'
            ]
        });
        const existingFacilityIds = existingFacilities.map((f)=>f.id);
        const invalidFacilityIds = fields.allowedFacilityIds.filter((id)=>!existingFacilityIds.includes(id));
        if (invalidFacilityIds.length > 0) {
            throw new _errors.ValidationError(`Invalid facility IDs: ${invalidFacilityIds.join(', ')}`);
        }
    }
    await db.transaction(async ()=>{
        const user = await User.create(fields);
        // Add new designations
        if (fields.designations && fields.designations.length > 0) {
            const designationRecords = fields.designations.map((designationId)=>({
                    userId: user.id,
                    designationId
                }));
            await UserDesignation.bulkCreate(designationRecords);
        }
        const uniqueFacilityIds = [
            ...new Set(fields.allowedFacilityIds || [])
        ];
        await UserFacility.bulkCreate(uniqueFacilityIds.map((facilityId)=>({
                userId: user.id,
                facilityId
            })), {
            ignoreDuplicates: true
        });
    });
    res.send({
        ok: true
    });
}));
const VALIDATION = _yup.object().shape({
    displayName: _yup.string().trim().required(),
    email: _yup.string().trim().email().required(),
    role: _yup.string().required()
}).noUnknown();
usersRouter.post('/validate', (0, _expressasynchandler.default)(async (req, res)=>{
    const { store: { models: { User, Permission } } } = req;
    // skip permission check as we will use it for both create and update api, and this is safe to do as it's only used for validation
    req.flagPermissionChecked();
    const fields = await VALIDATION.validate(req.body);
    const existingUserWithSameEmail = await User.findOne({
        where: {
            email: fields.email
        }
    });
    const existingUserWithSameDisplayName = await User.findOne({
        where: {
            displayName: {
                [_sequelize.Op.iLike]: fields.displayName
            }
        }
    });
    const writeUserPermission = await Permission.findOne({
        where: {
            roleId: fields.role,
            noun: 'User',
            verb: 'write'
        }
    });
    res.send({
        isEmailUnique: !existingUserWithSameEmail,
        isDisplayNameUnique: !existingUserWithSameDisplayName,
        hasWriteUserPermission: !!writeUserPermission
    });
}));
const UPDATE_VALIDATION = _yup.object().shape({
    visibilityStatus: _yup.string().required().oneOf([
        _constants.VISIBILITY_STATUSES.CURRENT,
        _constants.VISIBILITY_STATUSES.HISTORICAL
    ]),
    displayName: _yup.string().trim().required(),
    displayId: _yup.string().trim().nullable().optional(),
    role: _yup.string().required(),
    phoneNumber: _yup.string().trim().nullable().optional(),
    email: _yup.string().trim().email().required(),
    designations: _yup.array().of(_yup.string()).nullable().optional(),
    newPassword: _yup.string().nullable().optional(),
    confirmPassword: _yup.string().nullable().optional(),
    allowedFacilityIds: _yup.array().of(_yup.string()).nullable().optional()
}).test('passwords-match', 'Passwords must match', function(value) {
    const { newPassword, confirmPassword } = value;
    // If both passwords are provided, they must match
    if ((newPassword || confirmPassword) && newPassword !== confirmPassword) {
        return this.createError({
            message: 'Passwords must match'
        });
    }
    return true;
}).test('password-is-not-hashed', 'Password must not be hashed', function(value) {
    return !(0, _password.isBcryptHash)(value.newPassword);
}).noUnknown();
usersRouter.put('/:id', (0, _expressasynchandler.default)(async (req, res)=>{
    const { store: { models: { Role, User, UserDesignation, ReferenceData, UserFacility } }, params: { id }, db } = req;
    const fields = await UPDATE_VALIDATION.validate(req.body);
    const user = await User.findByPk(id);
    if (!user) {
        throw new _errors.NotFoundError('User not found');
    }
    // only allow updating the user if the user has the write permission for the all users
    req.checkPermission('write', (0, _ability.subject)('User', {
        id: String(Date.now())
    }));
    const role = await Role.findByPk(fields.role);
    if (!role) {
        throw new _errors.NotFoundError('Role not found');
    }
    const existingUserWithSameEmail = await User.findOne({
        where: {
            email: fields.email,
            id: {
                [_sequelize.Op.ne]: id
            }
        }
    });
    if (existingUserWithSameEmail) {
        throw new _errors.EditConflictError('Email must be unique across all users');
    }
    const existingUserWithSameDisplayName = await User.findOne({
        where: {
            displayName: {
                [_sequelize.Op.iLike]: fields.displayName
            },
            id: {
                [_sequelize.Op.ne]: id
            }
        }
    });
    if (existingUserWithSameDisplayName) {
        throw new _errors.EditConflictError('Display name must be unique across all users');
    }
    // Validate designations if provided
    if (fields.designations && fields.designations.length > 0) {
        // Check if all designation IDs exist and are of type 'designation'
        const existingDesignations = await ReferenceData.findAll({
            where: {
                id: {
                    [_sequelize.Op.in]: fields.designations
                },
                type: _constants.REFERENCE_TYPES.DESIGNATION,
                visibilityStatus: _constants.VISIBILITY_STATUSES.CURRENT
            },
            attributes: [
                'id'
            ]
        });
        const existingDesignationIds = existingDesignations.map((d)=>d.id);
        const invalidDesignationIds = fields.designations.filter((id)=>!existingDesignationIds.includes(id));
        if (invalidDesignationIds.length > 0) {
            throw new _errors.ValidationError(`Invalid designation IDs: ${invalidDesignationIds.join(', ')}`);
        }
    }
    const updateFields = {
        displayName: fields.displayName,
        role: fields.role,
        email: fields.email,
        visibilityStatus: fields.visibilityStatus,
        displayId: fields.displayId,
        phoneNumber: fields.phoneNumber
    };
    // Add password to update fields if provided
    if (fields.newPassword) {
        updateFields.password = fields.newPassword;
    }
    await db.transaction(async ()=>{
        await user.update(updateFields);
        // Remove existing designations
        await UserDesignation.destroy({
            where: {
                userId: id
            }
        });
        // Add new designations
        if (fields.designations && fields.designations.length > 0) {
            const designationRecords = fields.designations.map((designationId)=>({
                    userId: id,
                    designationId
                }));
            await UserDesignation.bulkCreate(designationRecords);
        }
        const uniqueFacilityIds = [
            ...new Set(fields.allowedFacilityIds || [])
        ];
        await updateUserFacilities(UserFacility, user, uniqueFacilityIds);
    });
    res.send({
        ok: true
    });
}));
const userLeaveSchema = _yup.object().shape({
    startDate: _yup.string().min(1, 'startDate is required').required(),
    endDate: _yup.string().min(1, 'endDate is required').required(),
    force: _yup.boolean().optional()
});
// POST /:id/leave - Create leave for a user
usersRouter.post('/:id/leaves', (0, _expressasynchandler.default)(async (req, res)=>{
    const { models, params, body, db } = req;
    const { id: userId } = params;
    const { UserLeave, LocationAssignment } = models;
    // only allow updating the user if the user has the write permission for the all users
    req.checkPermission('write', (0, _ability.subject)('User', {
        id: String(Date.now())
    }));
    const data = await userLeaveSchema.validate(body);
    const { startDate, endDate } = data;
    const parsedEndDate = (0, _datefns.startOfDay)(new Date(endDate));
    const currentDate = (0, _datefns.startOfDay)(new Date());
    if ((0, _datefns.isBefore)(parsedEndDate, currentDate)) {
        throw new _errors.InvalidOperationError('Cannot create leave in the past');
    }
    if (new Date(startDate) > new Date(endDate)) {
        throw new _errors.InvalidOperationError('startDate must be before or equal to endDate');
    }
    const leave = await db.transaction(async ()=>{
        // Check for overlapping leaves
        const overlap = await UserLeave.findOne({
            where: {
                userId,
                endDate: {
                    [_sequelize.Op.gte]: startDate
                },
                startDate: {
                    [_sequelize.Op.lte]: endDate
                }
            }
        });
        if (overlap) {
            throw new _errors.InvalidOperationError('Leave overlaps with an existing leave');
        }
        const leave = await UserLeave.create({
            userId,
            startDate,
            endDate
        });
        await LocationAssignment.destroy({
            where: {
                userId,
                date: {
                    [_sequelize.Op.between]: [
                        startDate,
                        endDate
                    ]
                }
            }
        });
        return leave;
    });
    res.send(leave);
}));
/**
 * Get all leaves for a user
 * If all is false, only upcoming leaves are returned
 */ usersRouter.get('/:id/leaves', (0, _expressasynchandler.default)(async (req, res)=>{
    const { models, params, query } = req;
    const { id: userId } = params;
    const user = await models.User.findByPk(userId);
    req.checkPermission('read', user);
    let where = {
        userId
    };
    if (query.all !== 'true') {
        where.endDate = {
            [_sequelize.Op.gte]: (0, _dateTime.getCurrentDateString)()
        };
    }
    const leaves = await models.UserLeave.findAll({
        where,
        order: [
            [
                'startDate',
                'ASC'
            ]
        ]
    });
    res.send(leaves);
}));
usersRouter.delete('/:id/leaves/:leaveId', (0, _expressasynchandler.default)(async (req, res)=>{
    const { models, params } = req;
    const { id: userId, leaveId } = params;
    // only allow updating the user if the user has the write permission for the all users
    req.checkPermission('write', (0, _ability.subject)('User', {
        id: String(Date.now())
    }));
    const leave = await models.UserLeave.findOne({
        where: {
            id: leaveId,
            userId
        }
    });
    if (!leave) {
        throw new _errors.NotFoundError('Leave not found');
    }
    // Delete the leave instead of marking it as removed
    await leave.destroy();
    res.send(leave);
}));
const getConflictingLocationAssignmentsSchema = _zod.default.object({
    after: _dateTime.dateCustomValidation,
    before: _dateTime.dateCustomValidation
});
usersRouter.get('/:id/conflicting-location-assignments', (0, _expressasynchandler.default)(async (req, res)=>{
    const { models, params } = req;
    const { id: userId } = params;
    const query = await getConflictingLocationAssignmentsSchema.parseAsync(req.query);
    const { after, before } = query;
    req.checkPermission('write', (0, _ability.subject)('User', {
        id: String(Date.now())
    }));
    const { LocationAssignment, User } = models;
    // Find location assignments that overlap with the leave period
    const conflictingAssignments = await LocationAssignment.findAll({
        include: [
            {
                model: User,
                as: 'user',
                attributes: [
                    'id'
                ]
            }
        ],
        where: {
            userId,
            date: {
                [_sequelize.Op.gte]: after,
                [_sequelize.Op.lte]: before
            }
        },
        attributes: [
            'id'
        ]
    });
    res.send({
        data: conflictingAssignments
    });
}));
async function updateUserFacilities(UserFacility, user, allowedFacilityIds) {
    if (allowedFacilityIds.length === 0) {
        return await UserFacility.destroy({
            where: {
                userId: user.id
            }
        });
    }
    await UserFacility.restore({
        where: {
            userId: user.id,
            facilityId: {
                [_sequelize.Op.in]: allowedFacilityIds
            }
        }
    });
    await UserFacility.bulkCreate(allowedFacilityIds.map((facilityId)=>({
            userId: user.id,
            facilityId
        })), {
        ignoreDuplicates: true
    });
    await UserFacility.destroy({
        where: {
            userId: user.id,
            facilityId: {
                [_sequelize.Op.notIn]: allowedFacilityIds
            }
        }
    });
}

//# sourceMappingURL=users.js.map