Autosave: 20260217-150839
This commit is contained in:
parent
39c16de175
commit
c1f17908e1
BIN
assets/pasted-20260217-143551-7fc7c6b1.png
Normal file
BIN
assets/pasted-20260217-143551-7fc7c6b1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@ -1,4 +1,3 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
@ -324,7 +323,7 @@ module.exports = class Office_calendar_eventsDBApi {
|
||||
{
|
||||
model: db.users,
|
||||
as: 'user',
|
||||
|
||||
required: false,
|
||||
where: filter.user ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -334,14 +333,14 @@ module.exports = class Office_calendar_eventsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
model: db.time_off_requests,
|
||||
as: 'time_off_request',
|
||||
|
||||
required: false,
|
||||
where: filter.time_off_request ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.time_off_request.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -351,14 +350,14 @@ module.exports = class Office_calendar_eventsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
model: db.holidays,
|
||||
as: 'holiday',
|
||||
|
||||
required: false,
|
||||
where: filter.holiday ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.holiday.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -368,7 +367,7 @@ module.exports = class Office_calendar_eventsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
@ -601,5 +600,4 @@ module.exports = class Office_calendar_eventsDBApi {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@ -424,7 +424,7 @@ module.exports = class Time_off_requestsDBApi {
|
||||
static async findAll(
|
||||
filter,
|
||||
options
|
||||
) {
|
||||
) { if (typeof filter.filter === 'string') { try { filter = { ...filter, ...JSON.parse(filter.filter) }; delete filter.filter; } catch (e) { console.error('Failed to parse filter JSON', e); } }
|
||||
const limit = filter.limit || 0;
|
||||
let offset = 0;
|
||||
let where = {};
|
||||
@ -795,7 +795,7 @@ module.exports = class Time_off_requestsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset, ) {
|
||||
static async findAllAutocomplete(query, limit, offset) {
|
||||
let where = {};
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('time_off_requests', 'is_taken', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.removeColumn('time_off_requests', 'is_taken');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('yearly_leave_summaries', 'medical_scheduled_days', {
|
||||
type: Sequelize.DECIMAL,
|
||||
defaultValue: 0,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.removeColumn('yearly_leave_summaries', 'medical_scheduled_days');
|
||||
}
|
||||
};
|
||||
@ -156,6 +156,12 @@ external_reference: {
|
||||
|
||||
},
|
||||
|
||||
is_taken: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
@ -255,6 +261,4 @@ external_reference: {
|
||||
|
||||
|
||||
return time_off_requests;
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -56,6 +56,12 @@ medical_taken_days: {
|
||||
|
||||
},
|
||||
|
||||
medical_scheduled_days: {
|
||||
type: DataTypes.DECIMAL,
|
||||
defaultValue: 0,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
bereavement_taken_days: {
|
||||
type: DataTypes.DECIMAL,
|
||||
|
||||
@ -144,4 +150,4 @@ ending_balance: {
|
||||
|
||||
|
||||
return yearly_leave_summaries;
|
||||
};
|
||||
};
|
||||
|
||||
@ -43,6 +43,7 @@ const appSettingsRoutes = require('./routes/app_settings');
|
||||
const loginBackgroundsRoutes = require('./routes/login_backgrounds');
|
||||
const checkLockout = require('./middlewares/lockout');
|
||||
|
||||
const Yearly_leave_summariesService = require('./services/yearly_leave_summaries');
|
||||
|
||||
const getBaseUrl = (url) => {
|
||||
if (!url) return '';
|
||||
@ -169,9 +170,25 @@ if (fs.existsSync(publicDir)) {
|
||||
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
||||
|
||||
db.sequelize.sync().then(function () {
|
||||
app.listen(PORT, () => {
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`Listening on port ${PORT}`);
|
||||
|
||||
// Initial run of summary updates
|
||||
try {
|
||||
await Yearly_leave_summariesService.updateAllSummaries();
|
||||
} catch (e) {
|
||||
console.error('Initial summary update failed', e);
|
||||
}
|
||||
|
||||
// Schedule periodic updates (every hour)
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await Yearly_leave_summariesService.updateAllSummaries();
|
||||
} catch (e) {
|
||||
console.error('Periodic summary update failed', e);
|
||||
}
|
||||
}, 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
module.exports = app;
|
||||
|
||||
@ -126,33 +126,33 @@ router.post('/bulk-import', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}/approve:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Approve the task
|
||||
* description: Approve the task
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to approve
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully approved
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}/approve:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Approve the task
|
||||
* description: Approve the task
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to approve
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully approved
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.put('/:id/approve', wrapAsync(async (req, res) => {
|
||||
await Approval_tasksService.approve(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
@ -160,53 +160,87 @@ router.put('/:id/approve', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Update the data of the selected item
|
||||
* description: Update the data of the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to update
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* description: Set new item data
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* description: ID of the updated item
|
||||
* type: string
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* required:
|
||||
* - id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item data was successfully updated
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}/reject:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Reject the task
|
||||
* description: Reject the task
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to reject
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully rejected
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.put('/:id/reject', wrapAsync(async (req, res) => {
|
||||
await Approval_tasksService.reject(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Update the data of the selected item
|
||||
* description: Update the data of the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to update
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* description: Set new item data
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* description: ID of the updated item
|
||||
* type: string
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* required:
|
||||
* - id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item data was successfully updated
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.put('/:id', wrapAsync(async (req, res) => {
|
||||
await Approval_tasksService.update(req.body.data, req.body.id, req.currentUser);
|
||||
const payload = true;
|
||||
@ -214,37 +248,37 @@ router.put('/:id', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* delete:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Delete the selected item
|
||||
* description: Delete the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to delete
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* delete:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Delete the selected item
|
||||
* description: Delete the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to delete
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.delete('/:id', wrapAsync(async (req, res) => {
|
||||
await Approval_tasksService.remove(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
@ -252,37 +286,37 @@ router.delete('/:id', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/deleteByIds:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Delete the selected item list
|
||||
* description: Delete the selected item list
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* ids:
|
||||
* description: IDs of the updated items
|
||||
* type: array
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Items not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/deleteByIds:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Delete the selected item list
|
||||
* description: Delete the selected item list
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* ids:
|
||||
* description: IDs of the updated items
|
||||
* type: array
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Items not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||
await Approval_tasksService.deleteByIds(req.body.data, req.currentUser);
|
||||
const payload = true;
|
||||
@ -290,29 +324,29 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Get all approval_tasks
|
||||
* description: Get all approval_tasks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
* @swagger
|
||||
* /api/approval_tasks:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Get all approval_tasks
|
||||
* description: Get all approval_tasks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/', wrapAsync(async (req, res) => {
|
||||
const filetype = req.query.filetype
|
||||
@ -343,30 +377,30 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/count:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Count all approval_tasks
|
||||
* description: Count all approval_tasks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks count successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/count:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Count all approval_tasks
|
||||
* description: Count all approval_tasks
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks count successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/count', wrapAsync(async (req, res) => {
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
@ -380,30 +414,30 @@ router.get('/count', wrapAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/autocomplete:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Find all approval_tasks that match search criteria
|
||||
* description: Find all approval_tasks that match search criteria
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/autocomplete:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Find all approval_tasks that match search criteria
|
||||
* description: Find all approval_tasks that match search criteria
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Approval_tasks list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/autocomplete', async (req, res) => {
|
||||
|
||||
const payload = await Approval_tasksDBApi.findAllAutocomplete(
|
||||
@ -417,37 +451,37 @@ router.get('/autocomplete', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Get selected item
|
||||
* description: Get selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: ID of item to get
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Selected item successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
* @swagger
|
||||
* /api/approval_tasks/{id}:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Approval_tasks]
|
||||
* summary: Get selected item
|
||||
* description: Get selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: ID of item to get
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Selected item successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Approval_tasks"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/:id', wrapAsync(async (req, res) => {
|
||||
const payload = await Approval_tasksDBApi.findBy(
|
||||
{ id: req.params.id },
|
||||
|
||||
@ -298,7 +298,7 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||
router.get('/', wrapAsync(async (req, res) => {
|
||||
const filetype = req.query.filetype
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
console.log('[DEBUG] GET /time_off_requests', req.query); const currentUser = req.currentUser;
|
||||
const payload = await Time_off_requestsDBApi.findAll(
|
||||
req.query, { currentUser }
|
||||
);
|
||||
@ -350,7 +350,7 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
*/
|
||||
router.get('/count', wrapAsync(async (req, res) => {
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
console.log('[DEBUG] GET /time_off_requests', req.query); const currentUser = req.currentUser;
|
||||
const payload = await Time_off_requestsDBApi.findAll(
|
||||
req.query,
|
||||
null,
|
||||
|
||||
@ -8,6 +8,7 @@ const config = require('../config');
|
||||
const stream = require('stream');
|
||||
const TimeOffApprovalEmail = require('./email/list/timeOffApproval');
|
||||
const EmailSender = require('./email');
|
||||
const Office_calendar_eventsDBApi = require('../db/api/office_calendar_events');
|
||||
|
||||
module.exports = class Approval_tasksService {
|
||||
static async create(data, currentUser) {
|
||||
@ -152,7 +153,19 @@ module.exports = class Approval_tasksService {
|
||||
await task.update({ state: 'completed', completed_at: new Date() }, { transaction });
|
||||
|
||||
if (task.time_off_request) {
|
||||
await task.time_off_request.update({ status: 'approved', decided_at: new Date() }, { transaction });
|
||||
const tor = task.time_off_request;
|
||||
await tor.update({ status: 'approved', decided_at: new Date() }, { transaction });
|
||||
|
||||
// Create calendar event
|
||||
await Office_calendar_eventsDBApi.create({
|
||||
event_type: 'time_off',
|
||||
title: `PTO - ${tor.requester?.firstName || ''} ${tor.requester?.lastName || ''}`,
|
||||
starts_at: tor.starts_at,
|
||||
ends_at: tor.ends_at,
|
||||
user: tor.requesterId,
|
||||
time_off_request: tor.id,
|
||||
is_all_day: true
|
||||
}, { currentUser, transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
@ -171,4 +184,40 @@ module.exports = class Approval_tasksService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async reject(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const task = await db.approval_tasks.findOne({
|
||||
where: { id },
|
||||
include: [
|
||||
{
|
||||
model: db.time_off_requests,
|
||||
as: 'time_off_request',
|
||||
include: [{ model: db.users, as: 'requester' }]
|
||||
}
|
||||
],
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new ValidationError('approval_tasksNotFound');
|
||||
}
|
||||
|
||||
await task.update({ state: 'completed', completed_at: new Date() }, { transaction });
|
||||
|
||||
if (task.time_off_request) {
|
||||
await task.time_off_request.update({ status: 'rejected', decided_at: new Date() }, { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// We could add a rejection email here if needed, but the user didn't ask for it specifically.
|
||||
// For consistency, we might want to eventually, but let's stick to the current request.
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -10,6 +10,7 @@ const config = require('../config');
|
||||
const stream = require('stream');
|
||||
const moment = require('moment');
|
||||
const Approval_tasksDBApi = require('../db/api/approval_tasks');
|
||||
const Office_calendar_eventsDBApi = require('../db/api/office_calendar_events');
|
||||
|
||||
|
||||
|
||||
@ -47,6 +48,14 @@ module.exports = class Time_off_requestsService {
|
||||
data.requires_approval = false;
|
||||
}
|
||||
|
||||
// Initial is_taken state
|
||||
if (data.status === 'approved' && data.starts_at) {
|
||||
const today = moment().startOf('day');
|
||||
if (moment(data.starts_at).isSameOrBefore(today)) {
|
||||
data.is_taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
const createdRequest = await Time_off_requestsDBApi.create(
|
||||
data,
|
||||
{
|
||||
@ -55,6 +64,19 @@ module.exports = class Time_off_requestsService {
|
||||
},
|
||||
);
|
||||
|
||||
// Create calendar event if approved
|
||||
if (createdRequest.status === 'approved') {
|
||||
await Office_calendar_eventsDBApi.create({
|
||||
event_type: 'time_off',
|
||||
title: `PTO - ${requester?.firstName || ''} ${requester?.lastName || ''}`,
|
||||
starts_at: createdRequest.starts_at,
|
||||
ends_at: createdRequest.ends_at,
|
||||
user: createdRequest.requesterId,
|
||||
time_off_request: createdRequest.id,
|
||||
is_all_day: true
|
||||
}, { currentUser, transaction });
|
||||
}
|
||||
|
||||
// Create approval task if requires_approval is true
|
||||
if (data.requires_approval !== false && createdRequest.status === 'pending_approval') {
|
||||
if (managerId) {
|
||||
@ -101,6 +123,10 @@ module.exports = class Time_off_requestsService {
|
||||
console.log('CSV results', results);
|
||||
resolve();
|
||||
})
|
||||
.on('end', async () => {
|
||||
console.log('CSV results', results);
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
})
|
||||
|
||||
@ -136,15 +162,20 @@ module.exports = class Time_off_requestsService {
|
||||
);
|
||||
}
|
||||
|
||||
const oldStatus = time_off_requests.status;
|
||||
|
||||
// Check if user is admin or if the request is in the past
|
||||
const isAdmin = currentUser.app_role?.name === config.roles.admin;
|
||||
const isPast = moment(time_off_requests.starts_at).isBefore(moment(), 'day');
|
||||
|
||||
if (!isAdmin && isPast) {
|
||||
throw new ValidationError(
|
||||
'errors.forbidden.message',
|
||||
'Cannot modify past time off requests. Please contact an administrator.',
|
||||
);
|
||||
// If we are just approving, maybe it's allowed?
|
||||
if (data.starts_at || data.ends_at) {
|
||||
throw new ValidationError(
|
||||
'errors.forbidden.message',
|
||||
'Cannot modify dates of past time off requests. Please contact an administrator.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate days if dates are changing
|
||||
@ -159,11 +190,27 @@ module.exports = class Time_off_requestsService {
|
||||
limit: 1000
|
||||
}, { transaction });
|
||||
|
||||
const workSchedule = currentUser.workSchedule || [1, 2, 3, 4, 5];
|
||||
const userId = time_off_requests.requesterId;
|
||||
const requester = await db.users.findByPk(userId, { transaction });
|
||||
const workSchedule = requester?.workSchedule || currentUser.workSchedule || [1, 2, 3, 4, 5];
|
||||
data.days = Time_off_requestsService.calculateWorkingDays(startsAt, endsAt, workSchedule, holidays.rows);
|
||||
}
|
||||
}
|
||||
|
||||
// Update is_taken if status is changing to approved or if dates are changing
|
||||
const newStatus = data.status || time_off_requests.status;
|
||||
const newStartsAt = data.starts_at || time_off_requests.starts_at;
|
||||
if (newStatus === 'approved' && newStartsAt) {
|
||||
const today = moment().startOf('day');
|
||||
if (moment(newStartsAt).isSameOrBefore(today)) {
|
||||
data.is_taken = true;
|
||||
} else {
|
||||
data.is_taken = false;
|
||||
}
|
||||
} else if (newStatus !== 'approved') {
|
||||
data.is_taken = false;
|
||||
}
|
||||
|
||||
const updatedTime_off_requests = await Time_off_requestsDBApi.update(
|
||||
id,
|
||||
data,
|
||||
@ -173,6 +220,35 @@ module.exports = class Time_off_requestsService {
|
||||
},
|
||||
);
|
||||
|
||||
// Create calendar event if status changed to approved
|
||||
if (newStatus === 'approved' && oldStatus !== 'approved') {
|
||||
const requester = await db.users.findByPk(updatedTime_off_requests.requesterId, { transaction });
|
||||
await Office_calendar_eventsDBApi.create({
|
||||
event_type: 'time_off',
|
||||
title: `PTO - ${requester?.firstName || ''} ${requester?.lastName || ''}`,
|
||||
starts_at: updatedTime_off_requests.starts_at,
|
||||
ends_at: updatedTime_off_requests.ends_at,
|
||||
user: updatedTime_off_requests.requesterId,
|
||||
time_off_request: updatedTime_off_requests.id,
|
||||
is_all_day: true
|
||||
}, { currentUser, transaction });
|
||||
} else if (newStatus !== 'approved' && oldStatus === 'approved') {
|
||||
// Delete calendar event if no longer approved
|
||||
await db.office_calendar_events.destroy({
|
||||
where: { time_off_requestId: id },
|
||||
transaction
|
||||
});
|
||||
} else if (newStatus === 'approved' && (data.starts_at || data.ends_at)) {
|
||||
// Update calendar event if dates changed
|
||||
await db.office_calendar_events.update({
|
||||
starts_at: updatedTime_off_requests.starts_at,
|
||||
ends_at: updatedTime_off_requests.ends_at
|
||||
}, {
|
||||
where: { time_off_requestId: id },
|
||||
transaction
|
||||
});
|
||||
}
|
||||
|
||||
// Handle cancellation: dismiss associated approval tasks
|
||||
if (data.status === 'cancelled') {
|
||||
const tasks = await db.approval_tasks.findAll({
|
||||
@ -243,6 +319,12 @@ module.exports = class Time_off_requestsService {
|
||||
transaction,
|
||||
});
|
||||
|
||||
// Also delete calendar events
|
||||
await db.office_calendar_events.destroy({
|
||||
where: { time_off_requestId: { [db.Sequelize.Op.in]: ids } },
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// Recalculate unique user/year pairs
|
||||
@ -291,6 +373,12 @@ module.exports = class Time_off_requestsService {
|
||||
},
|
||||
);
|
||||
|
||||
// Also delete calendar event
|
||||
await db.office_calendar_events.destroy({
|
||||
where: { time_off_requestId: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
if (requestToRecalculate && requestToRecalculate.userId && requestToRecalculate.year) {
|
||||
|
||||
@ -6,7 +6,7 @@ const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
const moment = require('moment'); // Import moment
|
||||
const moment = require('moment');
|
||||
|
||||
|
||||
|
||||
@ -45,7 +45,6 @@ module.exports = class Yearly_leave_summariesService {
|
||||
.pipe(csv())
|
||||
.on('data', (data) => results.push(data))
|
||||
.on('end', async () => {
|
||||
console.log('CSV results', results);
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
@ -133,9 +132,6 @@ module.exports = class Yearly_leave_summariesService {
|
||||
}
|
||||
|
||||
static async recalculate(userId, year) {
|
||||
// Run in a new transaction or just use default (autocommit for reads, but we write at the end)
|
||||
// For safety, we can wrap in a transaction, but calling this from another service that just committed is fine.
|
||||
// If we want atomic update, we use a transaction.
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const user = await db.users.findByPk(userId, { transaction });
|
||||
@ -155,7 +151,7 @@ module.exports = class Yearly_leave_summariesService {
|
||||
starts_at: {
|
||||
[db.Sequelize.Op.between]: [startOfYear, endOfYear]
|
||||
},
|
||||
deletedAt: null // Ensure we don't count deleted if paranoid
|
||||
deletedAt: null
|
||||
},
|
||||
transaction
|
||||
});
|
||||
@ -170,30 +166,69 @@ module.exports = class Yearly_leave_summariesService {
|
||||
transaction
|
||||
});
|
||||
|
||||
// Fetch holidays to accurately split partial requests
|
||||
const holidays = await db.holidays.findAll({
|
||||
where: {
|
||||
starts_at: {
|
||||
[db.Sequelize.Op.lte]: endOfYear
|
||||
},
|
||||
ends_at: {
|
||||
[db.Sequelize.Op.gte]: startOfYear
|
||||
}
|
||||
},
|
||||
transaction
|
||||
});
|
||||
|
||||
const workSchedule = user.workSchedule || [1, 2, 3, 4, 5];
|
||||
|
||||
let pto_pending = 0;
|
||||
let pto_scheduled = 0;
|
||||
let pto_taken = 0;
|
||||
let medical_taken = 0;
|
||||
let medical_scheduled = 0;
|
||||
let bereavement_taken = 0;
|
||||
|
||||
const Time_off_requestsService = require('./time_off_requests');
|
||||
|
||||
for (const req of requests) {
|
||||
const days = parseFloat(req.days) || 0;
|
||||
const isPTO = ['regular_pto', 'unplanned_pto'].includes(req.leave_type);
|
||||
const isMedical = req.leave_type === 'medical_leave';
|
||||
const start = moment(req.starts_at);
|
||||
const isBereavement = req.leave_type === 'bereavement';
|
||||
|
||||
// Pending: "total count of days... not approved" (Assuming Pending Approval)
|
||||
if (req.status === 'pending_approval') {
|
||||
if (isPTO) pto_pending += days;
|
||||
} else if (req.status === 'approved') {
|
||||
if (isPTO) {
|
||||
if (start.isAfter(today)) {
|
||||
pto_scheduled += days;
|
||||
} else {
|
||||
pto_taken += days;
|
||||
}
|
||||
} else if (isMedical) {
|
||||
if (start.isSameOrBefore(today)) {
|
||||
medical_taken += days;
|
||||
const startsAt = moment(req.starts_at).startOf('day');
|
||||
const endsAt = moment(req.ends_at).startOf('day');
|
||||
|
||||
if (today.isAfter(endsAt)) {
|
||||
// Fully in the past
|
||||
if (isPTO) pto_taken += days;
|
||||
else if (isMedical) medical_taken += days;
|
||||
else if (isBereavement) bereavement_taken += days;
|
||||
} else if (today.isBefore(startsAt)) {
|
||||
// Fully in the future
|
||||
if (isPTO) pto_scheduled += days;
|
||||
else if (isMedical) medical_scheduled += days;
|
||||
} else {
|
||||
// Currently happening! Split it day-by-day
|
||||
const takenDays = Time_off_requestsService.calculateWorkingDays(
|
||||
req.starts_at,
|
||||
today.toDate(),
|
||||
workSchedule,
|
||||
holidays
|
||||
);
|
||||
const remainingDays = Math.max(0, days - takenDays);
|
||||
|
||||
if (isPTO) {
|
||||
pto_taken += takenDays;
|
||||
pto_scheduled += remainingDays;
|
||||
} else if (isMedical) {
|
||||
medical_taken += takenDays;
|
||||
medical_scheduled += remainingDays;
|
||||
} else if (isBereavement) {
|
||||
bereavement_taken += takenDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,19 +237,11 @@ module.exports = class Yearly_leave_summariesService {
|
||||
// Calculate Adjustments
|
||||
let pto_adjustments = 0;
|
||||
for (const entry of journalEntries) {
|
||||
// Only consider PTO buckets for PTO Available
|
||||
if (entry.leave_bucket === 'regular_pto' || !entry.leave_bucket) {
|
||||
const amount = parseFloat(entry.amount_days) || 0;
|
||||
if (entry.entry_type === 'debit_manual_adjustment' || entry.entry_type === 'debit_time_off') {
|
||||
// Note: 'debit_time_off' is usually from requests. If we count requests separately, we shouldn't count this.
|
||||
// But currently requests don't create journal entries automatically.
|
||||
// If they did, we would double count.
|
||||
// Assuming for now manual entries are the main use of this table or 'credit_accrual'.
|
||||
// If 'debit_time_off' is used, check if it's linked to a request.
|
||||
if (entry.entry_type === 'debit_manual_adjustment') {
|
||||
pto_adjustments -= amount;
|
||||
}
|
||||
} else {
|
||||
if (entry.entry_type === 'debit_manual_adjustment') {
|
||||
pto_adjustments -= amount;
|
||||
} else if (entry.entry_type !== 'debit_time_off') {
|
||||
// credits
|
||||
pto_adjustments += amount;
|
||||
}
|
||||
@ -222,10 +249,6 @@ module.exports = class Yearly_leave_summariesService {
|
||||
}
|
||||
|
||||
const pto_limit = parseFloat(user.paid_pto_per_year) || 0;
|
||||
// Formula: Available = Limit + Adjustments - Taken - Pending - Scheduled
|
||||
// (Pending is subtracted as per user request: "pending pto + scheduled PTO" are subtracted)
|
||||
// Wait, "Available PTO = ... subtracted by PTO taken ... pending pto + scheduled PTO"
|
||||
// It implies (Limit - Taken) - (Pending + Scheduled). Same thing.
|
||||
const pto_available = pto_limit + pto_adjustments - pto_taken - pto_pending - pto_scheduled;
|
||||
|
||||
// Update or create summary
|
||||
@ -234,23 +257,23 @@ module.exports = class Yearly_leave_summariesService {
|
||||
transaction
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
pto_pending_days: pto_pending,
|
||||
pto_scheduled_days: pto_scheduled,
|
||||
pto_taken_days: pto_taken,
|
||||
pto_available_days: pto_available,
|
||||
medical_taken_days: medical_taken,
|
||||
medical_scheduled_days: medical_scheduled,
|
||||
bereavement_taken_days: bereavement_taken
|
||||
};
|
||||
|
||||
if (summary) {
|
||||
await summary.update({
|
||||
pto_pending_days: pto_pending,
|
||||
pto_scheduled_days: pto_scheduled,
|
||||
pto_taken_days: pto_taken,
|
||||
pto_available_days: pto_available,
|
||||
medical_taken_days: medical_taken
|
||||
}, { transaction });
|
||||
await summary.update(updateData, { transaction });
|
||||
} else {
|
||||
await db.yearly_leave_summaries.create({
|
||||
userId,
|
||||
calendar_year: year,
|
||||
pto_pending_days: pto_pending,
|
||||
pto_scheduled_days: pto_scheduled,
|
||||
pto_taken_days: pto_taken,
|
||||
pto_available_days: pto_available,
|
||||
medical_taken_days: medical_taken
|
||||
...updateData
|
||||
}, { transaction });
|
||||
}
|
||||
|
||||
@ -258,8 +281,49 @@ module.exports = class Yearly_leave_summariesService {
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('Error recalculating yearly leave summary:', error);
|
||||
// Don't throw, just log. Recalculation failure shouldn't block the main action if possible,
|
||||
// or maybe it should? For now, logging is safer to avoid blocking user actions if this logic is buggy.
|
||||
}
|
||||
}
|
||||
|
||||
static async updateAllSummaries() {
|
||||
const today = moment().startOf('day');
|
||||
console.log(`[CRON] Updating all summaries for date: ${today.format('YYYY-MM-DD')}`);
|
||||
|
||||
try {
|
||||
// 1. Mark requests as taken if they started today or before
|
||||
// This is still useful for historical marking, but recalculate now uses 'today' dynamically
|
||||
const [updatedCount] = await db.time_off_requests.update(
|
||||
{ is_taken: true },
|
||||
{
|
||||
where: {
|
||||
status: 'approved',
|
||||
is_taken: false,
|
||||
starts_at: {
|
||||
[db.Sequelize.Op.lte]: today.toDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (updatedCount > 0) {
|
||||
console.log(`[CRON] Marked ${updatedCount} requests as started.`);
|
||||
}
|
||||
|
||||
// 2. Recalculate all active summaries for current year and previous (to be safe)
|
||||
const currentYear = today.year();
|
||||
const summariesToUpdate = await db.yearly_leave_summaries.findAll({
|
||||
where: {
|
||||
calendar_year: {
|
||||
[db.Sequelize.Op.in]: [currentYear, currentYear - 1]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const summary of summariesToUpdate) {
|
||||
await this.recalculate(summary.userId, summary.calendar_year);
|
||||
}
|
||||
console.log(`[CRON] Recalculated ${summariesToUpdate.length} summaries.`);
|
||||
} catch (error) {
|
||||
console.error('[CRON] Error in updateAllSummaries:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -22,6 +22,9 @@ type TEvent = {
|
||||
title: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
event_type?: string;
|
||||
user?: any;
|
||||
holiday?: any;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -37,6 +40,19 @@ type Props = {
|
||||
'end-data-key': string;
|
||||
};
|
||||
|
||||
const stringToColor = (str: string) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
const BigCalendar = ({
|
||||
events,
|
||||
handleDeleteAction,
|
||||
@ -73,15 +89,27 @@ const BigCalendar = ({
|
||||
useEffect(() => {
|
||||
if (!events || !Array.isArray(events) || !events?.length) return;
|
||||
|
||||
const formattedEvents = events.map((event) => ({
|
||||
...event,
|
||||
start: new Date(event[startDataKey]),
|
||||
end: new Date(event[endDataKey]),
|
||||
title: event[showField],
|
||||
}));
|
||||
const formattedEvents = events.map((event) => {
|
||||
let title = event[showField];
|
||||
|
||||
if (entityName === 'office_calendar_events') {
|
||||
if (event.event_type === 'time_off' && event.user) {
|
||||
title = `${event.user.firstName} ${event.user.lastName} is off.`;
|
||||
} else if (event.event_type === 'holiday' && event.holiday) {
|
||||
title = event.holiday.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
start: new Date(event[startDataKey]),
|
||||
end: new Date(event[endDataKey]),
|
||||
title: title,
|
||||
};
|
||||
});
|
||||
|
||||
setMyEvents(formattedEvents);
|
||||
}, [endDataKey, events, startDataKey, showField]);
|
||||
}, [endDataKey, events, startDataKey, showField, entityName]);
|
||||
|
||||
const onRangeChange = (
|
||||
range: Date[] | { start: Date; end: Date },
|
||||
@ -114,8 +142,35 @@ const BigCalendar = ({
|
||||
onDateRangeChange(newRange);
|
||||
};
|
||||
|
||||
const eventPropGetter = (event: TEvent) => {
|
||||
let backgroundColor = '#3174ad';
|
||||
const color = 'white';
|
||||
|
||||
if (entityName === 'office_calendar_events') {
|
||||
if (event.event_type === 'time_off' && event.user) {
|
||||
backgroundColor = stringToColor(event.user.id);
|
||||
} else if (event.event_type === 'holiday') {
|
||||
backgroundColor = '#f0ad4e';
|
||||
} else {
|
||||
backgroundColor = '#5bc0de';
|
||||
}
|
||||
} else if (entityName === 'holidays') {
|
||||
backgroundColor = '#f0ad4e';
|
||||
}
|
||||
|
||||
return {
|
||||
style: {
|
||||
backgroundColor,
|
||||
color,
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
display: 'block'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='h-[600px] p-4'>
|
||||
<div className='h-[700px] p-4'>
|
||||
<Calendar
|
||||
defaultDate={defaultDate}
|
||||
defaultView={Views.MONTH}
|
||||
@ -125,6 +180,7 @@ const BigCalendar = ({
|
||||
onSelectSlot={handleCreateEventAction}
|
||||
onRangeChange={onRangeChange}
|
||||
scrollToTime={scrollToTime}
|
||||
eventPropGetter={eventPropGetter}
|
||||
components={{
|
||||
event: (props) => (
|
||||
<MyCustomEvent
|
||||
@ -152,22 +208,24 @@ const MyCustomEvent = (
|
||||
const { onDelete, hasUpdatePermission, title, event, pathEdit, pathView } = props;
|
||||
|
||||
return (
|
||||
<div className={'flex items-center justify-between relative'}>
|
||||
<div className={'flex items-center justify-between relative h-full px-1 py-0.5 text-xs'}>
|
||||
<Link
|
||||
href={`${pathView}${event.id}`}
|
||||
className={'text-ellipsis overflow-hidden grow'}
|
||||
className={'text-ellipsis overflow-hidden grow font-semibold'}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
<ListActionsPopover
|
||||
className={'w-2 h-2 text-white'}
|
||||
iconClassName={'text-white w-5'}
|
||||
itemId={event.id}
|
||||
onDelete={onDelete}
|
||||
pathEdit={`${pathEdit}${event.id}`}
|
||||
pathView={`${pathView}${event.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
<div className="ml-1 shrink-0">
|
||||
<ListActionsPopover
|
||||
className={'w-3 h-3 text-white opacity-70 hover:opacity-100'}
|
||||
iconClassName={'text-white w-3'}
|
||||
itemId={event.id}
|
||||
onDelete={onDelete}
|
||||
pathEdit={`${pathEdit}${event.id}`}
|
||||
pathView={`${pathView}${event.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,6 +15,9 @@ type Props = {
|
||||
children?: ReactNode
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
onDecline?: () => void
|
||||
declineButtonLabel?: string
|
||||
declineButtonColor?: ColorButtonKey
|
||||
}
|
||||
|
||||
const CardBoxModal = ({
|
||||
@ -25,6 +28,9 @@ const CardBoxModal = ({
|
||||
children,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onDecline,
|
||||
declineButtonLabel,
|
||||
declineButtonColor,
|
||||
}: Props) => {
|
||||
if (!isActive) {
|
||||
return null
|
||||
@ -33,6 +39,13 @@ const CardBoxModal = ({
|
||||
const footer = (
|
||||
<BaseButtons>
|
||||
<BaseButton label={buttonLabel} color={buttonColor} onClick={onConfirm} />
|
||||
{onDecline && (
|
||||
<BaseButton
|
||||
label={declineButtonLabel || 'Decline'}
|
||||
color={declineButtonColor || 'danger'}
|
||||
onClick={onDecline}
|
||||
/>
|
||||
)}
|
||||
{!!onCancel && <BaseButton label="Cancel" color={buttonColor} outline onClick={onCancel} />}
|
||||
</BaseButtons>
|
||||
)
|
||||
@ -56,4 +69,4 @@ const CardBoxModal = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default CardBoxModal
|
||||
export default CardBoxModal
|
||||
@ -1,5 +1,13 @@
|
||||
import React from 'react'
|
||||
import { mdiClockOutline, mdiCalendarCheck, mdiCalendarBlank, mdiMedicalBag, mdiCalendarArrowRight } from '@mdi/js'
|
||||
import {
|
||||
mdiClockOutline,
|
||||
mdiCalendarCheck,
|
||||
mdiCalendarBlank,
|
||||
mdiMedicalBag,
|
||||
mdiCalendarArrowRight,
|
||||
mdiDoctor,
|
||||
mdiHeart
|
||||
} from '@mdi/js'
|
||||
import CardBox from './CardBox'
|
||||
import BaseIcon from './BaseIcon'
|
||||
import Link from 'next/link'
|
||||
@ -12,13 +20,15 @@ type Props = {
|
||||
pto_taken_days: number | string
|
||||
pto_available_days: number | string
|
||||
medical_taken_days: number | string
|
||||
medical_scheduled_days?: number | string
|
||||
bereavement_taken_days?: number | string
|
||||
}
|
||||
}
|
||||
|
||||
const PTOStats = ({ summary }: Props) => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
|
||||
const stats = [
|
||||
const line1 = [
|
||||
{
|
||||
label: 'Pending PTO',
|
||||
value: summary?.pto_pending_days || 0,
|
||||
@ -46,46 +56,70 @@ const PTOStats = ({ summary }: Props) => {
|
||||
icon: mdiCalendarBlank,
|
||||
color: 'text-green-500',
|
||||
},
|
||||
]
|
||||
|
||||
const line2 = [
|
||||
{
|
||||
label: 'Medical Leave Taken',
|
||||
label: 'Medical Taken',
|
||||
value: summary?.medical_taken_days || 0,
|
||||
icon: mdiMedicalBag,
|
||||
color: 'text-red-500',
|
||||
href: `/time_off_requests/time_off_requests-list?leave_type=medical_leave&status=approved&requesterId=${currentUser?.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Medical Scheduled',
|
||||
value: summary?.medical_scheduled_days || 0,
|
||||
icon: mdiDoctor,
|
||||
color: 'text-orange-500',
|
||||
href: `/time_off_requests/time_off_requests-list?leave_type=medical_leave&status=approved&requesterId=${currentUser?.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Bereavement',
|
||||
value: summary?.bereavement_taken_days || 0,
|
||||
icon: mdiHeart,
|
||||
color: 'text-gray-500',
|
||||
href: `/time_off_requests/time_off_requests-list?leave_type=bereavement&status=approved&requesterId=${currentUser?.id}`,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3 lg:grid-cols-5 mb-6">
|
||||
{stats.map((stat, index) => {
|
||||
const content = (
|
||||
<div className="flex items-center justify-between h-full">
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-slate-400 text-sm">{stat.label}</p>
|
||||
<p className="text-2xl font-bold">{stat.value} Days</p>
|
||||
</div>
|
||||
<BaseIcon path={stat.icon} size={48} className={stat.color} />
|
||||
const renderStat = (stat, index) => {
|
||||
const content = (
|
||||
<div className="flex items-center justify-between h-full">
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-slate-400 text-sm whitespace-nowrap">{stat.label}</p>
|
||||
<p className="text-xl font-bold">{stat.value} Days</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (stat.href) {
|
||||
return (
|
||||
<Link key={index} href={stat.href} className="block hover:opacity-80 transition-opacity">
|
||||
<CardBox className="h-full">
|
||||
{content}
|
||||
</CardBox>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<BaseIcon path={stat.icon} size={24} className={stat.color} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (stat.href) {
|
||||
return (
|
||||
<CardBox key={index} className="h-full">
|
||||
{content}
|
||||
</CardBox>
|
||||
<Link key={index} href={stat.href} className="block hover:opacity-80 transition-opacity">
|
||||
<CardBox className="h-full p-4">
|
||||
{content}
|
||||
</CardBox>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBox key={index} className="h-full p-4">
|
||||
{content}
|
||||
</CardBox>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||
{line1.map(renderStat)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 mb-6">
|
||||
{line2.map(renderStat)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PTOStats
|
||||
export default PTOStats
|
||||
|
||||
@ -81,6 +81,7 @@ const Dashboard = () => {
|
||||
}, [])
|
||||
|
||||
const handleApprove = async (taskId) => {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||
// Refresh data
|
||||
@ -92,12 +93,25 @@ const Dashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async (taskId) => {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/reject`);
|
||||
// Refresh data
|
||||
fetchDashboardData();
|
||||
if (isReviewModalActive) setIsReviewModalActive(false);
|
||||
} catch (error) {
|
||||
console.error('Error rejecting task:', error);
|
||||
alert('Failed to reject task');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReview = (task) => {
|
||||
setSelectedTask(task);
|
||||
setIsReviewModalActive(true);
|
||||
};
|
||||
|
||||
const years = [selectedYear - 1, selectedYear, selectedYear + 1, selectedYear + 2]
|
||||
const years = [selectedYear - 1, selectedYear, selectedYear + 2]
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -220,6 +234,12 @@ const Dashboard = () => {
|
||||
small
|
||||
onClick={() => handleApprove(task.id)}
|
||||
/>
|
||||
<BaseButton
|
||||
color="danger"
|
||||
label="Decline"
|
||||
small
|
||||
onClick={() => handleReject(task.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@ -243,9 +263,12 @@ const Dashboard = () => {
|
||||
title="Review PTO Request"
|
||||
isActive={isReviewModalActive}
|
||||
onConfirm={() => handleApprove(selectedTask?.id)}
|
||||
onDecline={() => handleReject(selectedTask?.id)}
|
||||
onCancel={() => setIsReviewModalActive(false)}
|
||||
buttonColor="success"
|
||||
buttonLabel="Approve"
|
||||
declineButtonLabel="Decline"
|
||||
declineButtonColor="danger"
|
||||
>
|
||||
{selectedTask && (
|
||||
<div className="space-y-3">
|
||||
|
||||
@ -19,8 +19,6 @@ const EmployeeSummary = () => {
|
||||
const fetchSummaries = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// For now fetching all summaries for the year.
|
||||
// In a real app we might filter by manager if not admin.
|
||||
const res = await axios.get('/yearly_leave_summaries', {
|
||||
params: {
|
||||
limit: 100,
|
||||
@ -75,18 +73,20 @@ const EmployeeSummary = () => {
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700 bg-gray-50 dark:bg-dark-900">
|
||||
<th className="p-4">Employee Name</th>
|
||||
<th className="p-4">Pending</th>
|
||||
<th className="p-4">Scheduled</th>
|
||||
<th className="p-4">Pending PTO</th>
|
||||
<th className="p-4">Scheduled PTO</th>
|
||||
<th className="p-4">PTO Taken</th>
|
||||
<th className="p-4">Available</th>
|
||||
<th className="p-4">Available PTO</th>
|
||||
<th className="p-4">Medical Taken</th>
|
||||
<th className="p-4">Medical Scheduled</th>
|
||||
<th className="p-4 text-center">Bereavement</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summaries.length > 0 ? (
|
||||
summaries.map((summary) => (
|
||||
<tr key={summary.id} className="border-b dark:border-dark-700 hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors">
|
||||
<td className="p-4 font-medium">
|
||||
<tr key={summary.id} className="border-b dark:border-dark-700 hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors text-xs lg:text-sm">
|
||||
<td className="p-4 font-medium whitespace-nowrap">
|
||||
{summary.user?.firstName} {summary.user?.lastName}
|
||||
</td>
|
||||
<td className="p-4">{summary.pto_pending_days || 0}</td>
|
||||
@ -98,11 +98,17 @@ const EmployeeSummary = () => {
|
||||
<td className="p-4 text-red-600 dark:text-red-400">
|
||||
{summary.medical_taken_days || 0}
|
||||
</td>
|
||||
<td className="p-4 text-orange-600 dark:text-orange-400">
|
||||
{summary.medical_scheduled_days || 0}
|
||||
</td>
|
||||
<td className="p-4 text-center text-gray-500">
|
||||
{summary.bereavement_taken_days || 0}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="p-8 text-center text-gray-500">
|
||||
<td colSpan={8} className="p-8 text-center text-gray-500">
|
||||
No summaries found for {selectedYear}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -81,6 +81,7 @@ const Dashboard = () => {
|
||||
}, [])
|
||||
|
||||
const handleApprove = async (taskId) => {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||
// Refresh data
|
||||
@ -92,6 +93,19 @@ const Dashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async (taskId) => {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
await axios.put(`/approval_tasks/${taskId}/reject`);
|
||||
// Refresh data
|
||||
fetchDashboardData();
|
||||
if (isReviewModalActive) setIsReviewModalActive(false);
|
||||
} catch (error) {
|
||||
console.error('Error rejecting task:', error);
|
||||
alert('Failed to reject task');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReview = (task) => {
|
||||
setSelectedTask(task);
|
||||
setIsReviewModalActive(true);
|
||||
@ -220,6 +234,12 @@ const Dashboard = () => {
|
||||
small
|
||||
onClick={() => handleApprove(task.id)}
|
||||
/>
|
||||
<BaseButton
|
||||
color="danger"
|
||||
label="Decline"
|
||||
small
|
||||
onClick={() => handleReject(task.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@ -243,9 +263,12 @@ const Dashboard = () => {
|
||||
title="Review PTO Request"
|
||||
isActive={isReviewModalActive}
|
||||
onConfirm={() => handleApprove(selectedTask?.id)}
|
||||
onDecline={() => handleReject(selectedTask?.id)}
|
||||
onCancel={() => setIsReviewModalActive(false)}
|
||||
buttonColor="success"
|
||||
buttonLabel="Approve"
|
||||
declineButtonLabel="Decline"
|
||||
declineButtonColor="danger"
|
||||
>
|
||||
{selectedTask && (
|
||||
<div className="space-y-3">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, mdiMail, mdiUpload, mdiInformation } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import CardBox from '../../components/CardBox'
|
||||
@ -26,6 +26,7 @@ import { create } from '../../stores/time_off_requests/time_off_requestsSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
import BaseIcon from '../../components/BaseIcon'
|
||||
|
||||
const initialValues = {
|
||||
requester: '',
|
||||
@ -46,18 +47,36 @@ const initialValues = {
|
||||
external_reference: '',
|
||||
// Custom fields for UI
|
||||
date_requested: '',
|
||||
duration_type: 'all_day'
|
||||
starts_at_date: '',
|
||||
ends_at_date: '',
|
||||
duration_type: 'all_day',
|
||||
is_multiple: 'single'
|
||||
}
|
||||
|
||||
const DateDurationLogic = () => {
|
||||
const { values, setFieldValue } = useFormikContext<any>();
|
||||
|
||||
// Sync is_multiple with duration_type
|
||||
useEffect(() => {
|
||||
if (values.date_requested && values.duration_type) {
|
||||
if (values.is_multiple === 'multiple' && values.duration_type !== 'multiple_days') {
|
||||
setFieldValue('duration_type', 'multiple_days');
|
||||
} else if (values.is_multiple === 'single' && values.duration_type === 'multiple_days') {
|
||||
setFieldValue('duration_type', 'all_day');
|
||||
}
|
||||
}, [values.is_multiple, values.duration_type, setFieldValue]);
|
||||
|
||||
useEffect(() => {
|
||||
let start = '';
|
||||
let end = '';
|
||||
let days = 0;
|
||||
|
||||
if (values.duration_type === 'multiple_days') {
|
||||
if (values.starts_at_date && values.ends_at_date) {
|
||||
start = `${values.starts_at_date}T09:00`;
|
||||
end = `${values.ends_at_date}T17:00`;
|
||||
}
|
||||
} else if (values.date_requested && values.duration_type) {
|
||||
const date = values.date_requested;
|
||||
let start = '';
|
||||
let end = '';
|
||||
let days = 0;
|
||||
|
||||
if (values.duration_type === 'all_day') {
|
||||
start = `${date}T09:00`;
|
||||
@ -72,12 +91,13 @@ const DateDurationLogic = () => {
|
||||
end = `${date}T17:00`;
|
||||
days = 0.5;
|
||||
}
|
||||
|
||||
if (start !== values.starts_at) setFieldValue('starts_at', start);
|
||||
if (end !== values.ends_at) setFieldValue('ends_at', end);
|
||||
if (days !== values.days) setFieldValue('days', days);
|
||||
}
|
||||
}, [values.date_requested, values.duration_type, setFieldValue, values.starts_at, values.ends_at, values.days]);
|
||||
|
||||
if (start && start !== values.starts_at) setFieldValue('starts_at', start);
|
||||
if (end && end !== values.ends_at) setFieldValue('ends_at', end);
|
||||
if (days && days !== values.days) setFieldValue('days', days);
|
||||
|
||||
}, [values.date_requested, values.starts_at_date, values.ends_at_date, values.duration_type, setFieldValue, values.starts_at, values.ends_at, values.days]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -96,33 +116,35 @@ const Time_off_requestsNew = () => {
|
||||
requester: currentUser,
|
||||
submitted_at: moment().format('YYYY-MM-DDTHH:mm'),
|
||||
date_requested: moment().format('YYYY-MM-DD'),
|
||||
starts_at_date: moment().format('YYYY-MM-DD'),
|
||||
ends_at_date: moment().add(1, 'day').format('YYYY-MM-DD'),
|
||||
leave_type: (router.query.leave_type as string) || prev.leave_type
|
||||
}));
|
||||
}
|
||||
}, [currentUser, router.query]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
// Ensure hidden fields are set correctly if form didn't touch them
|
||||
// Note: DateDurationLogic handles starts_at/ends_at/days inside Formik,
|
||||
// so `data` should have them if they were updated.
|
||||
|
||||
// Fallback if date_requested is set but Logic didn't run (unlikely if rendered)
|
||||
const payload = { ...data };
|
||||
|
||||
if (!payload.starts_at && payload.date_requested) {
|
||||
const date = payload.date_requested;
|
||||
if (payload.duration_type === 'all_day') {
|
||||
payload.starts_at = `${date}T09:00`;
|
||||
payload.ends_at = `${date}T17:00`;
|
||||
payload.days = 1.0;
|
||||
} else if (payload.duration_type === 'am') {
|
||||
payload.starts_at = `${date}T09:00`;
|
||||
payload.ends_at = `${date}T13:00`;
|
||||
payload.days = 0.5;
|
||||
} else if (payload.duration_type === 'pm') {
|
||||
payload.starts_at = `${date}T13:00`;
|
||||
payload.ends_at = `${date}T17:00`;
|
||||
payload.days = 0.5;
|
||||
if (!payload.starts_at) {
|
||||
if (payload.duration_type === 'multiple_days' && payload.starts_at_date && payload.ends_at_date) {
|
||||
payload.starts_at = `${payload.starts_at_date}T09:00`;
|
||||
payload.ends_at = `${payload.ends_at_date}T17:00`;
|
||||
} else if (payload.date_requested) {
|
||||
const date = payload.date_requested;
|
||||
if (payload.duration_type === 'all_day') {
|
||||
payload.starts_at = `${date}T09:00`;
|
||||
payload.ends_at = `${date}T17:00`;
|
||||
payload.days = 1.0;
|
||||
} else if (payload.duration_type === 'am') {
|
||||
payload.starts_at = `${date}T09:00`;
|
||||
payload.ends_at = `${date}T13:00`;
|
||||
payload.days = 0.5;
|
||||
} else if (payload.duration_type === 'pm') {
|
||||
payload.starts_at = `${date}T13:00`;
|
||||
payload.ends_at = `${date}T17:00`;
|
||||
payload.days = 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,70 +179,109 @@ const Time_off_requestsNew = () => {
|
||||
initialValues={formInitialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<DateDurationLogic />
|
||||
{({ values, errors, touched }) => (
|
||||
<Form>
|
||||
<DateDurationLogic />
|
||||
|
||||
{/* Requester - Only visible to Admin */}
|
||||
{isAdmin && (
|
||||
<FormField label="Requester" labelFor="requester">
|
||||
<Field
|
||||
name="requester"
|
||||
id="requester"
|
||||
component={SelectField}
|
||||
options={currentUser}
|
||||
itemRef={'users'}
|
||||
showField="firstName"
|
||||
></Field>
|
||||
{/* Requester - Only visible to Admin */}
|
||||
{isAdmin && (
|
||||
<FormField label="Requester" labelFor="requester">
|
||||
<Field
|
||||
name="requester"
|
||||
id="requester"
|
||||
component={SelectField}
|
||||
options={currentUser}
|
||||
itemRef={'users'}
|
||||
showField="firstName"
|
||||
></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Leave Type" labelFor="leave_type">
|
||||
<Field name="leave_type" id="leave_type" component="select">
|
||||
<option value="regular_pto">Time Off Request / PTO Request</option>
|
||||
<option value="unplanned_pto">Unplanned PTO</option>
|
||||
<option value="medical_leave">Medical Leave</option>
|
||||
<option value="bereavement">Bereavement</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Leave Type" labelFor="leave_type">
|
||||
<Field name="leave_type" id="leave_type" component="select">
|
||||
<option value="regular_pto">Time Off Request / PTO Request</option>
|
||||
<option value="unplanned_pto">Unplanned PTO</option>
|
||||
<option value="medical_leave">Medical Leave</option>
|
||||
<option value="bereavement">Bereavement</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
{/* Duration Type Radio */}
|
||||
<FormField label="Duration">
|
||||
<FormCheckRadioGroup>
|
||||
<FormCheckRadio type="radio" label="Single Day">
|
||||
<Field type="radio" name="is_multiple" value="single" />
|
||||
</FormCheckRadio>
|
||||
<FormCheckRadio type="radio" label="Multiple Days">
|
||||
<Field type="radio" name="is_multiple" value="multiple" />
|
||||
</FormCheckRadio>
|
||||
</FormCheckRadioGroup>
|
||||
</FormField>
|
||||
|
||||
{/* Date Requested */}
|
||||
<FormField
|
||||
label="Date Requested"
|
||||
>
|
||||
<Field
|
||||
type="date"
|
||||
name="date_requested"
|
||||
placeholder="Date Requested"
|
||||
/>
|
||||
</FormField>
|
||||
{/* Conditional Fields based on is_multiple */}
|
||||
{values.is_multiple === 'multiple' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField label="Start Date">
|
||||
<Field
|
||||
type="date"
|
||||
name="starts_at_date"
|
||||
className={errors.starts_at_date && touched.starts_at_date ? 'border-red-500' : ''}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="End Date">
|
||||
<Field
|
||||
type="date"
|
||||
name="ends_at_date"
|
||||
className={errors.ends_at_date && touched.ends_at_date ? 'border-red-500' : ''}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<BaseIcon path={mdiInformation} size={20} className="mr-2" />
|
||||
<span>Working days will be calculated automatically based on your schedule and holidays.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField label="Date Requested">
|
||||
<Field
|
||||
type="date"
|
||||
name="date_requested"
|
||||
placeholder="Date Requested"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Day Part">
|
||||
<Field name="duration_type" component="select">
|
||||
<option value="all_day">All Day</option>
|
||||
<option value="am">AM (Half Day)</option>
|
||||
<option value="pm">PM (Half Day)</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Duration Select */}
|
||||
<FormField label="Duration" labelFor="duration_type">
|
||||
<Field name="duration_type" id="duration_type" component="select">
|
||||
<option value="all_day">All Day</option>
|
||||
<option value="am">AM</option>
|
||||
<option value="pm">PM</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
{/* Hidden Fields for Backend */}
|
||||
<div style={{ display: 'none' }}>
|
||||
<FormField label="StartsAt"><Field type="datetime-local" name="starts_at" /></FormField>
|
||||
<FormField label="EndsAt"><Field type="datetime-local" name="ends_at" /></FormField>
|
||||
<FormField label="Days"><Field type="number" name="days" /></FormField>
|
||||
</div>
|
||||
|
||||
{/* Hidden Fields for Backend */}
|
||||
<div style={{ display: 'none' }}>
|
||||
<FormField label="StartsAt"><Field type="datetime-local" name="starts_at" /></FormField>
|
||||
<FormField label="EndsAt"><Field type="datetime-local" name="ends_at" /></FormField>
|
||||
<FormField label="Days"><Field type="number" name="days" /></FormField>
|
||||
</div>
|
||||
<FormField label="Reason" hasTextareaHeight>
|
||||
<Field name="reason" as="textarea" placeholder="Reason" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Reason" hasTextareaHeight>
|
||||
<Field name="reason" as="textarea" placeholder="Reason" />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/time_off_requests/time_off_requests-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/time_off_requests/time_off_requests-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user