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 db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -324,7 +323,7 @@ module.exports = class Office_calendar_eventsDBApi {
|
|||||||
{
|
{
|
||||||
model: db.users,
|
model: db.users,
|
||||||
as: 'user',
|
as: 'user',
|
||||||
|
required: false,
|
||||||
where: filter.user ? {
|
where: filter.user ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
|
{ 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,
|
model: db.time_off_requests,
|
||||||
as: 'time_off_request',
|
as: 'time_off_request',
|
||||||
|
required: false,
|
||||||
where: filter.time_off_request ? {
|
where: filter.time_off_request ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.time_off_request.split('|').map(term => Utils.uuid(term)) } },
|
{ 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,
|
model: db.holidays,
|
||||||
as: 'holiday',
|
as: 'holiday',
|
||||||
|
required: false,
|
||||||
where: filter.holiday ? {
|
where: filter.holiday ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.holiday.split('|').map(term => Utils.uuid(term)) } },
|
{ 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(
|
static async findAll(
|
||||||
filter,
|
filter,
|
||||||
options
|
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;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
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 = {};
|
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: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -255,6 +261,4 @@ external_reference: {
|
|||||||
|
|
||||||
|
|
||||||
return time_off_requests;
|
return time_off_requests;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +56,12 @@ medical_taken_days: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
medical_scheduled_days: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
defaultValue: 0,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
|
||||||
bereavement_taken_days: {
|
bereavement_taken_days: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
@ -144,4 +150,4 @@ ending_balance: {
|
|||||||
|
|
||||||
|
|
||||||
return yearly_leave_summaries;
|
return yearly_leave_summaries;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,6 +43,7 @@ const appSettingsRoutes = require('./routes/app_settings');
|
|||||||
const loginBackgroundsRoutes = require('./routes/login_backgrounds');
|
const loginBackgroundsRoutes = require('./routes/login_backgrounds');
|
||||||
const checkLockout = require('./middlewares/lockout');
|
const checkLockout = require('./middlewares/lockout');
|
||||||
|
|
||||||
|
const Yearly_leave_summariesService = require('./services/yearly_leave_summaries');
|
||||||
|
|
||||||
const getBaseUrl = (url) => {
|
const getBaseUrl = (url) => {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
@ -169,9 +170,25 @@ if (fs.existsSync(publicDir)) {
|
|||||||
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
||||||
|
|
||||||
db.sequelize.sync().then(function () {
|
db.sequelize.sync().then(function () {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
console.log(`Listening on port ${PORT}`);
|
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
|
* @swagger
|
||||||
* /api/approval_tasks/{id}/approve:
|
* /api/approval_tasks/{id}/approve:
|
||||||
* put:
|
* put:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Approve the task
|
* summary: Approve the task
|
||||||
* description: Approve the task
|
* description: Approve the task
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
* description: Item ID to approve
|
* description: Item ID to approve
|
||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: The item was successfully approved
|
* description: The item was successfully approved
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid ID supplied
|
* description: Invalid ID supplied
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Item not found
|
* description: Item not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.put('/:id/approve', wrapAsync(async (req, res) => {
|
router.put('/:id/approve', wrapAsync(async (req, res) => {
|
||||||
await Approval_tasksService.approve(req.params.id, req.currentUser);
|
await Approval_tasksService.approve(req.params.id, req.currentUser);
|
||||||
const payload = true;
|
const payload = true;
|
||||||
@ -160,53 +160,87 @@ router.put('/:id/approve', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/{id}:
|
* /api/approval_tasks/{id}/reject:
|
||||||
* put:
|
* put:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Update the data of the selected item
|
* summary: Reject the task
|
||||||
* description: Update the data of the selected item
|
* description: Reject the task
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
* description: Item ID to update
|
* description: Item ID to reject
|
||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* requestBody:
|
* responses:
|
||||||
* description: Set new item data
|
* 200:
|
||||||
* required: true
|
* description: The item was successfully rejected
|
||||||
* content:
|
* 400:
|
||||||
* application/json:
|
* description: Invalid ID supplied
|
||||||
* schema:
|
* 401:
|
||||||
* properties:
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* id:
|
* 404:
|
||||||
* description: ID of the updated item
|
* description: Item not found
|
||||||
* type: string
|
* 500:
|
||||||
* data:
|
* description: Some server error
|
||||||
* description: Data of the updated item
|
*/
|
||||||
* type: object
|
router.put('/:id/reject', wrapAsync(async (req, res) => {
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
await Approval_tasksService.reject(req.params.id, req.currentUser);
|
||||||
* required:
|
const payload = true;
|
||||||
* - id
|
res.status(200).send(payload);
|
||||||
* responses:
|
}));
|
||||||
* 200:
|
|
||||||
* description: The item data was successfully updated
|
/**
|
||||||
* content:
|
* @swagger
|
||||||
* application/json:
|
* /api/approval_tasks/{id}:
|
||||||
* schema:
|
* put:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* security:
|
||||||
* 400:
|
* - bearerAuth: []
|
||||||
* description: Invalid ID supplied
|
* tags: [Approval_tasks]
|
||||||
* 401:
|
* summary: Update the data of the selected item
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* description: Update the data of the selected item
|
||||||
* 404:
|
* parameters:
|
||||||
* description: Item not found
|
* - in: path
|
||||||
* 500:
|
* name: id
|
||||||
* description: Some server error
|
* 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) => {
|
router.put('/:id', wrapAsync(async (req, res) => {
|
||||||
await Approval_tasksService.update(req.body.data, req.body.id, req.currentUser);
|
await Approval_tasksService.update(req.body.data, req.body.id, req.currentUser);
|
||||||
const payload = true;
|
const payload = true;
|
||||||
@ -214,37 +248,37 @@ router.put('/:id', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/{id}:
|
* /api/approval_tasks/{id}:
|
||||||
* delete:
|
* delete:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Delete the selected item
|
* summary: Delete the selected item
|
||||||
* description: Delete the selected item
|
* description: Delete the selected item
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
* description: Item ID to delete
|
* description: Item ID to delete
|
||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: The item was successfully deleted
|
* description: The item was successfully deleted
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid ID supplied
|
* description: Invalid ID supplied
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Item not found
|
* description: Item not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.delete('/:id', wrapAsync(async (req, res) => {
|
router.delete('/:id', wrapAsync(async (req, res) => {
|
||||||
await Approval_tasksService.remove(req.params.id, req.currentUser);
|
await Approval_tasksService.remove(req.params.id, req.currentUser);
|
||||||
const payload = true;
|
const payload = true;
|
||||||
@ -252,37 +286,37 @@ router.delete('/:id', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/deleteByIds:
|
* /api/approval_tasks/deleteByIds:
|
||||||
* post:
|
* post:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Delete the selected item list
|
* summary: Delete the selected item list
|
||||||
* description: Delete the selected item list
|
* description: Delete the selected item list
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* properties:
|
* properties:
|
||||||
* ids:
|
* ids:
|
||||||
* description: IDs of the updated items
|
* description: IDs of the updated items
|
||||||
* type: array
|
* type: array
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: The items was successfully deleted
|
* description: The items was successfully deleted
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Items not found
|
* description: Items not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||||
await Approval_tasksService.deleteByIds(req.body.data, req.currentUser);
|
await Approval_tasksService.deleteByIds(req.body.data, req.currentUser);
|
||||||
const payload = true;
|
const payload = true;
|
||||||
@ -290,29 +324,29 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks:
|
* /api/approval_tasks:
|
||||||
* get:
|
* get:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Get all approval_tasks
|
* summary: Get all approval_tasks
|
||||||
* description: Get all approval_tasks
|
* description: Get all approval_tasks
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Approval_tasks list successfully received
|
* description: Approval_tasks list successfully received
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Data not found
|
* description: Data not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/', wrapAsync(async (req, res) => {
|
router.get('/', wrapAsync(async (req, res) => {
|
||||||
const filetype = req.query.filetype
|
const filetype = req.query.filetype
|
||||||
@ -343,30 +377,30 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/count:
|
* /api/approval_tasks/count:
|
||||||
* get:
|
* get:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Count all approval_tasks
|
* summary: Count all approval_tasks
|
||||||
* description: Count all approval_tasks
|
* description: Count all approval_tasks
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Approval_tasks count successfully received
|
* description: Approval_tasks count successfully received
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Data not found
|
* description: Data not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/count', wrapAsync(async (req, res) => {
|
router.get('/count', wrapAsync(async (req, res) => {
|
||||||
|
|
||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
@ -380,30 +414,30 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/autocomplete:
|
* /api/approval_tasks/autocomplete:
|
||||||
* get:
|
* get:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Find all approval_tasks that match search criteria
|
* summary: Find all approval_tasks that match search criteria
|
||||||
* description: Find all approval_tasks that match search criteria
|
* description: Find all approval_tasks that match search criteria
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Approval_tasks list successfully received
|
* description: Approval_tasks list successfully received
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Data not found
|
* description: Data not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/autocomplete', async (req, res) => {
|
router.get('/autocomplete', async (req, res) => {
|
||||||
|
|
||||||
const payload = await Approval_tasksDBApi.findAllAutocomplete(
|
const payload = await Approval_tasksDBApi.findAllAutocomplete(
|
||||||
@ -417,37 +451,37 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/approval_tasks/{id}:
|
* /api/approval_tasks/{id}:
|
||||||
* get:
|
* get:
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* tags: [Approval_tasks]
|
* tags: [Approval_tasks]
|
||||||
* summary: Get selected item
|
* summary: Get selected item
|
||||||
* description: Get selected item
|
* description: Get selected item
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
* description: ID of item to get
|
* description: ID of item to get
|
||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Selected item successfully received
|
* description: Selected item successfully received
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: "#/components/schemas/Approval_tasks"
|
* $ref: "#/components/schemas/Approval_tasks"
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid ID supplied
|
* description: Invalid ID supplied
|
||||||
* 401:
|
* 401:
|
||||||
* $ref: "#/components/responses/UnauthorizedError"
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
* 404:
|
* 404:
|
||||||
* description: Item not found
|
* description: Item not found
|
||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
const payload = await Approval_tasksDBApi.findBy(
|
const payload = await Approval_tasksDBApi.findBy(
|
||||||
{ id: req.params.id },
|
{ id: req.params.id },
|
||||||
|
|||||||
@ -298,7 +298,7 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
|||||||
router.get('/', wrapAsync(async (req, res) => {
|
router.get('/', wrapAsync(async (req, res) => {
|
||||||
const filetype = req.query.filetype
|
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(
|
const payload = await Time_off_requestsDBApi.findAll(
|
||||||
req.query, { currentUser }
|
req.query, { currentUser }
|
||||||
);
|
);
|
||||||
@ -350,7 +350,7 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get('/count', 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(
|
const payload = await Time_off_requestsDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const config = require('../config');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const TimeOffApprovalEmail = require('./email/list/timeOffApproval');
|
const TimeOffApprovalEmail = require('./email/list/timeOffApproval');
|
||||||
const EmailSender = require('./email');
|
const EmailSender = require('./email');
|
||||||
|
const Office_calendar_eventsDBApi = require('../db/api/office_calendar_events');
|
||||||
|
|
||||||
module.exports = class Approval_tasksService {
|
module.exports = class Approval_tasksService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -152,7 +153,19 @@ module.exports = class Approval_tasksService {
|
|||||||
await task.update({ state: 'completed', completed_at: new Date() }, { transaction });
|
await task.update({ state: 'completed', completed_at: new Date() }, { transaction });
|
||||||
|
|
||||||
if (task.time_off_request) {
|
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();
|
await transaction.commit();
|
||||||
@ -171,4 +184,40 @@ module.exports = class Approval_tasksService {
|
|||||||
throw error;
|
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 stream = require('stream');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const Approval_tasksDBApi = require('../db/api/approval_tasks');
|
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;
|
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(
|
const createdRequest = await Time_off_requestsDBApi.create(
|
||||||
data,
|
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
|
// Create approval task if requires_approval is true
|
||||||
if (data.requires_approval !== false && createdRequest.status === 'pending_approval') {
|
if (data.requires_approval !== false && createdRequest.status === 'pending_approval') {
|
||||||
if (managerId) {
|
if (managerId) {
|
||||||
@ -101,6 +123,10 @@ module.exports = class Time_off_requestsService {
|
|||||||
console.log('CSV results', results);
|
console.log('CSV results', results);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
|
.on('end', async () => {
|
||||||
|
console.log('CSV results', results);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
.on('error', (error) => reject(error));
|
.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
|
// Check if user is admin or if the request is in the past
|
||||||
const isAdmin = currentUser.app_role?.name === config.roles.admin;
|
const isAdmin = currentUser.app_role?.name === config.roles.admin;
|
||||||
const isPast = moment(time_off_requests.starts_at).isBefore(moment(), 'day');
|
const isPast = moment(time_off_requests.starts_at).isBefore(moment(), 'day');
|
||||||
|
|
||||||
if (!isAdmin && isPast) {
|
if (!isAdmin && isPast) {
|
||||||
throw new ValidationError(
|
// If we are just approving, maybe it's allowed?
|
||||||
'errors.forbidden.message',
|
if (data.starts_at || data.ends_at) {
|
||||||
'Cannot modify past time off requests. Please contact an administrator.',
|
throw new ValidationError(
|
||||||
);
|
'errors.forbidden.message',
|
||||||
|
'Cannot modify dates of past time off requests. Please contact an administrator.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate days if dates are changing
|
// Recalculate days if dates are changing
|
||||||
@ -159,11 +190,27 @@ module.exports = class Time_off_requestsService {
|
|||||||
limit: 1000
|
limit: 1000
|
||||||
}, { transaction });
|
}, { 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);
|
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(
|
const updatedTime_off_requests = await Time_off_requestsDBApi.update(
|
||||||
id,
|
id,
|
||||||
data,
|
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
|
// Handle cancellation: dismiss associated approval tasks
|
||||||
if (data.status === 'cancelled') {
|
if (data.status === 'cancelled') {
|
||||||
const tasks = await db.approval_tasks.findAll({
|
const tasks = await db.approval_tasks.findAll({
|
||||||
@ -243,6 +319,12 @@ module.exports = class Time_off_requestsService {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also delete calendar events
|
||||||
|
await db.office_calendar_events.destroy({
|
||||||
|
where: { time_off_requestId: { [db.Sequelize.Op.in]: ids } },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
// Recalculate unique user/year pairs
|
// 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();
|
await transaction.commit();
|
||||||
|
|
||||||
if (requestToRecalculate && requestToRecalculate.userId && requestToRecalculate.year) {
|
if (requestToRecalculate && requestToRecalculate.userId && requestToRecalculate.year) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const csv = require('csv-parser');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
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())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
@ -133,9 +132,6 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async recalculate(userId, year) {
|
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();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const user = await db.users.findByPk(userId, { transaction });
|
const user = await db.users.findByPk(userId, { transaction });
|
||||||
@ -155,7 +151,7 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
starts_at: {
|
starts_at: {
|
||||||
[db.Sequelize.Op.between]: [startOfYear, endOfYear]
|
[db.Sequelize.Op.between]: [startOfYear, endOfYear]
|
||||||
},
|
},
|
||||||
deletedAt: null // Ensure we don't count deleted if paranoid
|
deletedAt: null
|
||||||
},
|
},
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -170,30 +166,69 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
transaction
|
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_pending = 0;
|
||||||
let pto_scheduled = 0;
|
let pto_scheduled = 0;
|
||||||
let pto_taken = 0;
|
let pto_taken = 0;
|
||||||
let medical_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) {
|
for (const req of requests) {
|
||||||
const days = parseFloat(req.days) || 0;
|
const days = parseFloat(req.days) || 0;
|
||||||
const isPTO = ['regular_pto', 'unplanned_pto'].includes(req.leave_type);
|
const isPTO = ['regular_pto', 'unplanned_pto'].includes(req.leave_type);
|
||||||
const isMedical = req.leave_type === 'medical_leave';
|
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 (req.status === 'pending_approval') {
|
||||||
if (isPTO) pto_pending += days;
|
if (isPTO) pto_pending += days;
|
||||||
} else if (req.status === 'approved') {
|
} else if (req.status === 'approved') {
|
||||||
if (isPTO) {
|
const startsAt = moment(req.starts_at).startOf('day');
|
||||||
if (start.isAfter(today)) {
|
const endsAt = moment(req.ends_at).startOf('day');
|
||||||
pto_scheduled += days;
|
|
||||||
} else {
|
if (today.isAfter(endsAt)) {
|
||||||
pto_taken += days;
|
// Fully in the past
|
||||||
}
|
if (isPTO) pto_taken += days;
|
||||||
} else if (isMedical) {
|
else if (isMedical) medical_taken += days;
|
||||||
if (start.isSameOrBefore(today)) {
|
else if (isBereavement) bereavement_taken += days;
|
||||||
medical_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
|
// Calculate Adjustments
|
||||||
let pto_adjustments = 0;
|
let pto_adjustments = 0;
|
||||||
for (const entry of journalEntries) {
|
for (const entry of journalEntries) {
|
||||||
// Only consider PTO buckets for PTO Available
|
|
||||||
if (entry.leave_bucket === 'regular_pto' || !entry.leave_bucket) {
|
if (entry.leave_bucket === 'regular_pto' || !entry.leave_bucket) {
|
||||||
const amount = parseFloat(entry.amount_days) || 0;
|
const amount = parseFloat(entry.amount_days) || 0;
|
||||||
if (entry.entry_type === 'debit_manual_adjustment' || entry.entry_type === 'debit_time_off') {
|
if (entry.entry_type === 'debit_manual_adjustment') {
|
||||||
// Note: 'debit_time_off' is usually from requests. If we count requests separately, we shouldn't count this.
|
pto_adjustments -= amount;
|
||||||
// But currently requests don't create journal entries automatically.
|
} else if (entry.entry_type !== 'debit_time_off') {
|
||||||
// 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 {
|
|
||||||
// credits
|
// credits
|
||||||
pto_adjustments += amount;
|
pto_adjustments += amount;
|
||||||
}
|
}
|
||||||
@ -222,10 +249,6 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pto_limit = parseFloat(user.paid_pto_per_year) || 0;
|
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;
|
const pto_available = pto_limit + pto_adjustments - pto_taken - pto_pending - pto_scheduled;
|
||||||
|
|
||||||
// Update or create summary
|
// Update or create summary
|
||||||
@ -234,23 +257,23 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
transaction
|
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) {
|
if (summary) {
|
||||||
await summary.update({
|
await summary.update(updateData, { transaction });
|
||||||
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 });
|
|
||||||
} else {
|
} else {
|
||||||
await db.yearly_leave_summaries.create({
|
await db.yearly_leave_summaries.create({
|
||||||
userId,
|
userId,
|
||||||
calendar_year: year,
|
calendar_year: year,
|
||||||
pto_pending_days: pto_pending,
|
...updateData
|
||||||
pto_scheduled_days: pto_scheduled,
|
|
||||||
pto_taken_days: pto_taken,
|
|
||||||
pto_available_days: pto_available,
|
|
||||||
medical_taken_days: medical_taken
|
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,8 +281,49 @@ module.exports = class Yearly_leave_summariesService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
console.error('Error recalculating yearly leave summary:', error);
|
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;
|
title: string;
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
|
event_type?: string;
|
||||||
|
user?: any;
|
||||||
|
holiday?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -37,6 +40,19 @@ type Props = {
|
|||||||
'end-data-key': string;
|
'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 = ({
|
const BigCalendar = ({
|
||||||
events,
|
events,
|
||||||
handleDeleteAction,
|
handleDeleteAction,
|
||||||
@ -73,15 +89,27 @@ const BigCalendar = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!events || !Array.isArray(events) || !events?.length) return;
|
if (!events || !Array.isArray(events) || !events?.length) return;
|
||||||
|
|
||||||
const formattedEvents = events.map((event) => ({
|
const formattedEvents = events.map((event) => {
|
||||||
...event,
|
let title = event[showField];
|
||||||
start: new Date(event[startDataKey]),
|
|
||||||
end: new Date(event[endDataKey]),
|
if (entityName === 'office_calendar_events') {
|
||||||
title: event[showField],
|
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);
|
setMyEvents(formattedEvents);
|
||||||
}, [endDataKey, events, startDataKey, showField]);
|
}, [endDataKey, events, startDataKey, showField, entityName]);
|
||||||
|
|
||||||
const onRangeChange = (
|
const onRangeChange = (
|
||||||
range: Date[] | { start: Date; end: Date },
|
range: Date[] | { start: Date; end: Date },
|
||||||
@ -114,8 +142,35 @@ const BigCalendar = ({
|
|||||||
onDateRangeChange(newRange);
|
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 (
|
return (
|
||||||
<div className='h-[600px] p-4'>
|
<div className='h-[700px] p-4'>
|
||||||
<Calendar
|
<Calendar
|
||||||
defaultDate={defaultDate}
|
defaultDate={defaultDate}
|
||||||
defaultView={Views.MONTH}
|
defaultView={Views.MONTH}
|
||||||
@ -125,6 +180,7 @@ const BigCalendar = ({
|
|||||||
onSelectSlot={handleCreateEventAction}
|
onSelectSlot={handleCreateEventAction}
|
||||||
onRangeChange={onRangeChange}
|
onRangeChange={onRangeChange}
|
||||||
scrollToTime={scrollToTime}
|
scrollToTime={scrollToTime}
|
||||||
|
eventPropGetter={eventPropGetter}
|
||||||
components={{
|
components={{
|
||||||
event: (props) => (
|
event: (props) => (
|
||||||
<MyCustomEvent
|
<MyCustomEvent
|
||||||
@ -152,22 +208,24 @@ const MyCustomEvent = (
|
|||||||
const { onDelete, hasUpdatePermission, title, event, pathEdit, pathView } = props;
|
const { onDelete, hasUpdatePermission, title, event, pathEdit, pathView } = props;
|
||||||
|
|
||||||
return (
|
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
|
<Link
|
||||||
href={`${pathView}${event.id}`}
|
href={`${pathView}${event.id}`}
|
||||||
className={'text-ellipsis overflow-hidden grow'}
|
className={'text-ellipsis overflow-hidden grow font-semibold'}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
<ListActionsPopover
|
<div className="ml-1 shrink-0">
|
||||||
className={'w-2 h-2 text-white'}
|
<ListActionsPopover
|
||||||
iconClassName={'text-white w-5'}
|
className={'w-3 h-3 text-white opacity-70 hover:opacity-100'}
|
||||||
itemId={event.id}
|
iconClassName={'text-white w-3'}
|
||||||
onDelete={onDelete}
|
itemId={event.id}
|
||||||
pathEdit={`${pathEdit}${event.id}`}
|
onDelete={onDelete}
|
||||||
pathView={`${pathView}${event.id}`}
|
pathEdit={`${pathEdit}${event.id}`}
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
pathView={`${pathView}${event.id}`}
|
||||||
/>
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,6 +15,9 @@ type Props = {
|
|||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
|
onDecline?: () => void
|
||||||
|
declineButtonLabel?: string
|
||||||
|
declineButtonColor?: ColorButtonKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardBoxModal = ({
|
const CardBoxModal = ({
|
||||||
@ -25,6 +28,9 @@ const CardBoxModal = ({
|
|||||||
children,
|
children,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onDecline,
|
||||||
|
declineButtonLabel,
|
||||||
|
declineButtonColor,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
return null
|
return null
|
||||||
@ -33,6 +39,13 @@ const CardBoxModal = ({
|
|||||||
const footer = (
|
const footer = (
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton label={buttonLabel} color={buttonColor} onClick={onConfirm} />
|
<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} />}
|
{!!onCancel && <BaseButton label="Cancel" color={buttonColor} outline onClick={onCancel} />}
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
)
|
)
|
||||||
@ -56,4 +69,4 @@ const CardBoxModal = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CardBoxModal
|
export default CardBoxModal
|
||||||
@ -1,5 +1,13 @@
|
|||||||
import React from 'react'
|
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 CardBox from './CardBox'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@ -12,13 +20,15 @@ type Props = {
|
|||||||
pto_taken_days: number | string
|
pto_taken_days: number | string
|
||||||
pto_available_days: number | string
|
pto_available_days: number | string
|
||||||
medical_taken_days: number | string
|
medical_taken_days: number | string
|
||||||
|
medical_scheduled_days?: number | string
|
||||||
|
bereavement_taken_days?: number | string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PTOStats = ({ summary }: Props) => {
|
const PTOStats = ({ summary }: Props) => {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth)
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
|
||||||
const stats = [
|
const line1 = [
|
||||||
{
|
{
|
||||||
label: 'Pending PTO',
|
label: 'Pending PTO',
|
||||||
value: summary?.pto_pending_days || 0,
|
value: summary?.pto_pending_days || 0,
|
||||||
@ -46,46 +56,70 @@ const PTOStats = ({ summary }: Props) => {
|
|||||||
icon: mdiCalendarBlank,
|
icon: mdiCalendarBlank,
|
||||||
color: 'text-green-500',
|
color: 'text-green-500',
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const line2 = [
|
||||||
{
|
{
|
||||||
label: 'Medical Leave Taken',
|
label: 'Medical Taken',
|
||||||
value: summary?.medical_taken_days || 0,
|
value: summary?.medical_taken_days || 0,
|
||||||
icon: mdiMedicalBag,
|
icon: mdiMedicalBag,
|
||||||
color: 'text-red-500',
|
color: 'text-red-500',
|
||||||
href: `/time_off_requests/time_off_requests-list?leave_type=medical_leave&status=approved&requesterId=${currentUser?.id}`,
|
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 (
|
const renderStat = (stat, index) => {
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3 lg:grid-cols-5 mb-6">
|
const content = (
|
||||||
{stats.map((stat, index) => {
|
<div className="flex items-center justify-between h-full">
|
||||||
const content = (
|
<div>
|
||||||
<div className="flex items-center justify-between h-full">
|
<p className="text-gray-500 dark:text-slate-400 text-sm whitespace-nowrap">{stat.label}</p>
|
||||||
<div>
|
<p className="text-xl font-bold">{stat.value} Days</p>
|
||||||
<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} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<BaseIcon path={stat.icon} size={24} className={stat.color} />
|
||||||
|
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (stat.href) {
|
||||||
return (
|
return (
|
||||||
<CardBox key={index} className="h-full">
|
<Link key={index} href={stat.href} className="block hover:opacity-80 transition-opacity">
|
||||||
{content}
|
<CardBox className="h-full p-4">
|
||||||
</CardBox>
|
{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) => {
|
const handleApprove = async (taskId) => {
|
||||||
|
if (!taskId) return;
|
||||||
try {
|
try {
|
||||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||||
// Refresh data
|
// 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) => {
|
const handleReview = (task) => {
|
||||||
setSelectedTask(task);
|
setSelectedTask(task);
|
||||||
setIsReviewModalActive(true);
|
setIsReviewModalActive(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const years = [selectedYear - 1, selectedYear, selectedYear + 1, selectedYear + 2]
|
const years = [selectedYear - 1, selectedYear, selectedYear + 2]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -220,6 +234,12 @@ const Dashboard = () => {
|
|||||||
small
|
small
|
||||||
onClick={() => handleApprove(task.id)}
|
onClick={() => handleApprove(task.id)}
|
||||||
/>
|
/>
|
||||||
|
<BaseButton
|
||||||
|
color="danger"
|
||||||
|
label="Decline"
|
||||||
|
small
|
||||||
|
onClick={() => handleReject(task.id)}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@ -243,9 +263,12 @@ const Dashboard = () => {
|
|||||||
title="Review PTO Request"
|
title="Review PTO Request"
|
||||||
isActive={isReviewModalActive}
|
isActive={isReviewModalActive}
|
||||||
onConfirm={() => handleApprove(selectedTask?.id)}
|
onConfirm={() => handleApprove(selectedTask?.id)}
|
||||||
|
onDecline={() => handleReject(selectedTask?.id)}
|
||||||
onCancel={() => setIsReviewModalActive(false)}
|
onCancel={() => setIsReviewModalActive(false)}
|
||||||
buttonColor="success"
|
buttonColor="success"
|
||||||
buttonLabel="Approve"
|
buttonLabel="Approve"
|
||||||
|
declineButtonLabel="Decline"
|
||||||
|
declineButtonColor="danger"
|
||||||
>
|
>
|
||||||
{selectedTask && (
|
{selectedTask && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@ -19,8 +19,6 @@ const EmployeeSummary = () => {
|
|||||||
const fetchSummaries = async () => {
|
const fetchSummaries = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
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', {
|
const res = await axios.get('/yearly_leave_summaries', {
|
||||||
params: {
|
params: {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
@ -75,18 +73,20 @@ const EmployeeSummary = () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b dark:border-dark-700 bg-gray-50 dark:bg-dark-900">
|
<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">Employee Name</th>
|
||||||
<th className="p-4">Pending</th>
|
<th className="p-4">Pending PTO</th>
|
||||||
<th className="p-4">Scheduled</th>
|
<th className="p-4">Scheduled PTO</th>
|
||||||
<th className="p-4">PTO Taken</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 Taken</th>
|
||||||
|
<th className="p-4">Medical Scheduled</th>
|
||||||
|
<th className="p-4 text-center">Bereavement</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{summaries.length > 0 ? (
|
{summaries.length > 0 ? (
|
||||||
summaries.map((summary) => (
|
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">
|
<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">
|
<td className="p-4 font-medium whitespace-nowrap">
|
||||||
{summary.user?.firstName} {summary.user?.lastName}
|
{summary.user?.firstName} {summary.user?.lastName}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">{summary.pto_pending_days || 0}</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">
|
<td className="p-4 text-red-600 dark:text-red-400">
|
||||||
{summary.medical_taken_days || 0}
|
{summary.medical_taken_days || 0}
|
||||||
</td>
|
</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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<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}
|
No summaries found for {selectedYear}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -81,6 +81,7 @@ const Dashboard = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleApprove = async (taskId) => {
|
const handleApprove = async (taskId) => {
|
||||||
|
if (!taskId) return;
|
||||||
try {
|
try {
|
||||||
await axios.put(`/approval_tasks/${taskId}/approve`);
|
await axios.put(`/approval_tasks/${taskId}/approve`);
|
||||||
// Refresh data
|
// 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) => {
|
const handleReview = (task) => {
|
||||||
setSelectedTask(task);
|
setSelectedTask(task);
|
||||||
setIsReviewModalActive(true);
|
setIsReviewModalActive(true);
|
||||||
@ -220,6 +234,12 @@ const Dashboard = () => {
|
|||||||
small
|
small
|
||||||
onClick={() => handleApprove(task.id)}
|
onClick={() => handleApprove(task.id)}
|
||||||
/>
|
/>
|
||||||
|
<BaseButton
|
||||||
|
color="danger"
|
||||||
|
label="Decline"
|
||||||
|
small
|
||||||
|
onClick={() => handleReject(task.id)}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@ -243,9 +263,12 @@ const Dashboard = () => {
|
|||||||
title="Review PTO Request"
|
title="Review PTO Request"
|
||||||
isActive={isReviewModalActive}
|
isActive={isReviewModalActive}
|
||||||
onConfirm={() => handleApprove(selectedTask?.id)}
|
onConfirm={() => handleApprove(selectedTask?.id)}
|
||||||
|
onDecline={() => handleReject(selectedTask?.id)}
|
||||||
onCancel={() => setIsReviewModalActive(false)}
|
onCancel={() => setIsReviewModalActive(false)}
|
||||||
buttonColor="success"
|
buttonColor="success"
|
||||||
buttonLabel="Approve"
|
buttonLabel="Approve"
|
||||||
|
declineButtonLabel="Decline"
|
||||||
|
declineButtonColor="danger"
|
||||||
>
|
>
|
||||||
{selectedTask && (
|
{selectedTask && (
|
||||||
<div className="space-y-3">
|
<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 Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
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 { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import BaseIcon from '../../components/BaseIcon'
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
requester: '',
|
requester: '',
|
||||||
@ -46,18 +47,36 @@ const initialValues = {
|
|||||||
external_reference: '',
|
external_reference: '',
|
||||||
// Custom fields for UI
|
// Custom fields for UI
|
||||||
date_requested: '',
|
date_requested: '',
|
||||||
duration_type: 'all_day'
|
starts_at_date: '',
|
||||||
|
ends_at_date: '',
|
||||||
|
duration_type: 'all_day',
|
||||||
|
is_multiple: 'single'
|
||||||
}
|
}
|
||||||
|
|
||||||
const DateDurationLogic = () => {
|
const DateDurationLogic = () => {
|
||||||
const { values, setFieldValue } = useFormikContext<any>();
|
const { values, setFieldValue } = useFormikContext<any>();
|
||||||
|
|
||||||
|
// Sync is_multiple with duration_type
|
||||||
useEffect(() => {
|
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;
|
const date = values.date_requested;
|
||||||
let start = '';
|
|
||||||
let end = '';
|
|
||||||
let days = 0;
|
|
||||||
|
|
||||||
if (values.duration_type === 'all_day') {
|
if (values.duration_type === 'all_day') {
|
||||||
start = `${date}T09:00`;
|
start = `${date}T09:00`;
|
||||||
@ -72,12 +91,13 @@ const DateDurationLogic = () => {
|
|||||||
end = `${date}T17:00`;
|
end = `${date}T17:00`;
|
||||||
days = 0.5;
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
@ -96,33 +116,35 @@ const Time_off_requestsNew = () => {
|
|||||||
requester: currentUser,
|
requester: currentUser,
|
||||||
submitted_at: moment().format('YYYY-MM-DDTHH:mm'),
|
submitted_at: moment().format('YYYY-MM-DDTHH:mm'),
|
||||||
date_requested: moment().format('YYYY-MM-DD'),
|
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
|
leave_type: (router.query.leave_type as string) || prev.leave_type
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [currentUser, router.query]);
|
}, [currentUser, router.query]);
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
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 };
|
const payload = { ...data };
|
||||||
|
|
||||||
if (!payload.starts_at && payload.date_requested) {
|
if (!payload.starts_at) {
|
||||||
const date = payload.date_requested;
|
if (payload.duration_type === 'multiple_days' && payload.starts_at_date && payload.ends_at_date) {
|
||||||
if (payload.duration_type === 'all_day') {
|
payload.starts_at = `${payload.starts_at_date}T09:00`;
|
||||||
payload.starts_at = `${date}T09:00`;
|
payload.ends_at = `${payload.ends_at_date}T17:00`;
|
||||||
payload.ends_at = `${date}T17:00`;
|
} else if (payload.date_requested) {
|
||||||
payload.days = 1.0;
|
const date = payload.date_requested;
|
||||||
} else if (payload.duration_type === 'am') {
|
if (payload.duration_type === 'all_day') {
|
||||||
payload.starts_at = `${date}T09:00`;
|
payload.starts_at = `${date}T09:00`;
|
||||||
payload.ends_at = `${date}T13:00`;
|
payload.ends_at = `${date}T17:00`;
|
||||||
payload.days = 0.5;
|
payload.days = 1.0;
|
||||||
} else if (payload.duration_type === 'pm') {
|
} else if (payload.duration_type === 'am') {
|
||||||
payload.starts_at = `${date}T13:00`;
|
payload.starts_at = `${date}T09:00`;
|
||||||
payload.ends_at = `${date}T17:00`;
|
payload.ends_at = `${date}T13:00`;
|
||||||
payload.days = 0.5;
|
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}
|
initialValues={formInitialValues}
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
{({ values, errors, touched }) => (
|
||||||
<DateDurationLogic />
|
<Form>
|
||||||
|
<DateDurationLogic />
|
||||||
|
|
||||||
{/* Requester - Only visible to Admin */}
|
{/* Requester - Only visible to Admin */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<FormField label="Requester" labelFor="requester">
|
<FormField label="Requester" labelFor="requester">
|
||||||
<Field
|
<Field
|
||||||
name="requester"
|
name="requester"
|
||||||
id="requester"
|
id="requester"
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={currentUser}
|
options={currentUser}
|
||||||
itemRef={'users'}
|
itemRef={'users'}
|
||||||
showField="firstName"
|
showField="firstName"
|
||||||
></Field>
|
></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>
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField label="Leave Type" labelFor="leave_type">
|
{/* Duration Type Radio */}
|
||||||
<Field name="leave_type" id="leave_type" component="select">
|
<FormField label="Duration">
|
||||||
<option value="regular_pto">Time Off Request / PTO Request</option>
|
<FormCheckRadioGroup>
|
||||||
<option value="unplanned_pto">Unplanned PTO</option>
|
<FormCheckRadio type="radio" label="Single Day">
|
||||||
<option value="medical_leave">Medical Leave</option>
|
<Field type="radio" name="is_multiple" value="single" />
|
||||||
<option value="bereavement">Bereavement</option>
|
</FormCheckRadio>
|
||||||
</Field>
|
<FormCheckRadio type="radio" label="Multiple Days">
|
||||||
</FormField>
|
<Field type="radio" name="is_multiple" value="multiple" />
|
||||||
|
</FormCheckRadio>
|
||||||
|
</FormCheckRadioGroup>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
{/* Date Requested */}
|
{/* Conditional Fields based on is_multiple */}
|
||||||
<FormField
|
{values.is_multiple === 'multiple' ? (
|
||||||
label="Date Requested"
|
<div className="space-y-4">
|
||||||
>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Field
|
<FormField label="Start Date">
|
||||||
type="date"
|
<Field
|
||||||
name="date_requested"
|
type="date"
|
||||||
placeholder="Date Requested"
|
name="starts_at_date"
|
||||||
/>
|
className={errors.starts_at_date && touched.starts_at_date ? 'border-red-500' : ''}
|
||||||
</FormField>
|
/>
|
||||||
|
</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 */}
|
{/* Hidden Fields for Backend */}
|
||||||
<FormField label="Duration" labelFor="duration_type">
|
<div style={{ display: 'none' }}>
|
||||||
<Field name="duration_type" id="duration_type" component="select">
|
<FormField label="StartsAt"><Field type="datetime-local" name="starts_at" /></FormField>
|
||||||
<option value="all_day">All Day</option>
|
<FormField label="EndsAt"><Field type="datetime-local" name="ends_at" /></FormField>
|
||||||
<option value="am">AM</option>
|
<FormField label="Days"><Field type="number" name="days" /></FormField>
|
||||||
<option value="pm">PM</option>
|
</div>
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
{/* Hidden Fields for Backend */}
|
<FormField label="Reason" hasTextareaHeight>
|
||||||
<div style={{ display: 'none' }}>
|
<Field name="reason" as="textarea" placeholder="Reason" />
|
||||||
<FormField label="StartsAt"><Field type="datetime-local" name="starts_at" /></FormField>
|
</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>
|
<BaseDivider />
|
||||||
<Field name="reason" as="textarea" placeholder="Reason" />
|
<BaseButtons>
|
||||||
</FormField>
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
<BaseDivider />
|
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/time_off_requests/time_off_requests-list')}/>
|
||||||
<BaseButtons>
|
</BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
</Form>
|
||||||
<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>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user