From c1f17908e11516f6fdfac86dd4ea724b73fbb3a2 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 15:08:39 +0000 Subject: [PATCH] Autosave: 20260217-150839 --- assets/pasted-20260217-143551-7fc7c6b1.png | Bin 0 -> 55021 bytes backend/src/db/api/office_calendar_events.js | 16 +- backend/src/db/api/time_off_requests.js | 4 +- ...20260217125031-add-is-taken-to-requests.js | 15 + ...5217-add-medical-scheduled-to-summaries.js | 15 + backend/src/db/models/time_off_requests.js | 10 +- .../src/db/models/yearly_leave_summaries.js | 8 +- backend/src/index.js | 21 +- backend/src/routes/approval_tasks.js | 510 ++++++++++-------- backend/src/routes/time_off_requests.js | 4 +- backend/src/services/approval_tasks.js | 51 +- backend/src/services/time_off_requests.js | 98 +++- .../src/services/yearly_leave_summaries.js | 156 ++++-- frontend/src/components/BigCalendar.tsx | 96 +++- frontend/src/components/CardBoxModal.tsx | 15 +- frontend/src/components/PTOStats.tsx | 94 ++-- frontend/src/pages/dashboard.tsx | 25 +- frontend/src/pages/employee-summary.tsx | 22 +- frontend/src/pages/index.tsx | 23 + .../time_off_requests-new.tsx | 235 +++++--- 20 files changed, 963 insertions(+), 455 deletions(-) create mode 100644 assets/pasted-20260217-143551-7fc7c6b1.png create mode 100644 backend/src/db/migrations/20260217125031-add-is-taken-to-requests.js create mode 100644 backend/src/db/migrations/20260217125217-add-medical-scheduled-to-summaries.js diff --git a/assets/pasted-20260217-143551-7fc7c6b1.png b/assets/pasted-20260217-143551-7fc7c6b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3926b7db92c42f20c2636d27a2f3d73f8cb480a0 GIT binary patch literal 55021 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 YjDy}Ab^K@o{sZw&NJ_Bqt+v 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 { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/time_off_requests.js b/backend/src/db/api/time_off_requests.js index 1d803c9..14e6ac4 100644 --- a/backend/src/db/api/time_off_requests.js +++ b/backend/src/db/api/time_off_requests.js @@ -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 = {}; diff --git a/backend/src/db/migrations/20260217125031-add-is-taken-to-requests.js b/backend/src/db/migrations/20260217125031-add-is-taken-to-requests.js new file mode 100644 index 0000000..ae1be1d --- /dev/null +++ b/backend/src/db/migrations/20260217125031-add-is-taken-to-requests.js @@ -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'); + } +}; \ No newline at end of file diff --git a/backend/src/db/migrations/20260217125217-add-medical-scheduled-to-summaries.js b/backend/src/db/migrations/20260217125217-add-medical-scheduled-to-summaries.js new file mode 100644 index 0000000..4765b70 --- /dev/null +++ b/backend/src/db/migrations/20260217125217-add-medical-scheduled-to-summaries.js @@ -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'); + } +}; \ No newline at end of file diff --git a/backend/src/db/models/time_off_requests.js b/backend/src/db/models/time_off_requests.js index cf8ed2c..90b7361 100644 --- a/backend/src/db/models/time_off_requests.js +++ b/backend/src/db/models/time_off_requests.js @@ -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; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/models/yearly_leave_summaries.js b/backend/src/db/models/yearly_leave_summaries.js index f004139..fda1e4f 100644 --- a/backend/src/db/models/yearly_leave_summaries.js +++ b/backend/src/db/models/yearly_leave_summaries.js @@ -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; -}; \ No newline at end of file +}; diff --git a/backend/src/index.js b/backend/src/index.js index 0c13254..85dac0f 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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; \ No newline at end of file +module.exports = app; diff --git a/backend/src/routes/approval_tasks.js b/backend/src/routes/approval_tasks.js index 47fdb40..f03e5f5 100644 --- a/backend/src/routes/approval_tasks.js +++ b/backend/src/routes/approval_tasks.js @@ -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 }, diff --git a/backend/src/routes/time_off_requests.js b/backend/src/routes/time_off_requests.js index 90c292e..2e2e85d 100644 --- a/backend/src/routes/time_off_requests.js +++ b/backend/src/routes/time_off_requests.js @@ -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, diff --git a/backend/src/services/approval_tasks.js b/backend/src/services/approval_tasks.js index 42eaa08..43e7fb5 100644 --- a/backend/src/services/approval_tasks.js +++ b/backend/src/services/approval_tasks.js @@ -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; + } + } }; \ No newline at end of file diff --git a/backend/src/services/time_off_requests.js b/backend/src/services/time_off_requests.js index 974088c..1a9561e 100644 --- a/backend/src/services/time_off_requests.js +++ b/backend/src/services/time_off_requests.js @@ -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) { diff --git a/backend/src/services/yearly_leave_summaries.js b/backend/src/services/yearly_leave_summaries.js index 6e3a335..cd6e0af 100644 --- a/backend/src/services/yearly_leave_summaries.js +++ b/backend/src/services/yearly_leave_summaries.js @@ -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); + } + } }; \ No newline at end of file diff --git a/frontend/src/components/BigCalendar.tsx b/frontend/src/components/BigCalendar.tsx index a457be6..7f22f66 100644 --- a/frontend/src/components/BigCalendar.tsx +++ b/frontend/src/components/BigCalendar.tsx @@ -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 ( -
+
( +
{title} - +
+ +
); }; diff --git a/frontend/src/components/CardBoxModal.tsx b/frontend/src/components/CardBoxModal.tsx index 27d5676..c441b5b 100644 --- a/frontend/src/components/CardBoxModal.tsx +++ b/frontend/src/components/CardBoxModal.tsx @@ -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 = ( + {onDecline && ( + + )} {!!onCancel && } ) @@ -56,4 +69,4 @@ const CardBoxModal = ({ ) } -export default CardBoxModal +export default CardBoxModal \ No newline at end of file diff --git a/frontend/src/components/PTOStats.tsx b/frontend/src/components/PTOStats.tsx index fecbab3..61f5804 100644 --- a/frontend/src/components/PTOStats.tsx +++ b/frontend/src/components/PTOStats.tsx @@ -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 ( -
- {stats.map((stat, index) => { - const content = ( -
-
-

{stat.label}

-

{stat.value} Days

-
- + const renderStat = (stat, index) => { + const content = ( +
+
+

{stat.label}

+

{stat.value} Days

- ); - - if (stat.href) { - return ( - - - {content} - - - ) - } + +
+ ); + if (stat.href) { return ( - - {content} - + + + {content} + + ) - })} -
+ } + + return ( + + {content} + + ) + } + + return ( + <> +
+ {line1.map(renderStat)} +
+
+ {line2.map(renderStat)} +
+ ) } -export default PTOStats \ No newline at end of file +export default PTOStats diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 2eae058..bcc56c3 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -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)} /> + handleReject(task.id)} + /> )) @@ -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 && (
diff --git a/frontend/src/pages/employee-summary.tsx b/frontend/src/pages/employee-summary.tsx index 820b676..83e2967 100644 --- a/frontend/src/pages/employee-summary.tsx +++ b/frontend/src/pages/employee-summary.tsx @@ -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 = () => { Employee Name - Pending - Scheduled + Pending PTO + Scheduled PTO PTO Taken - Available + Available PTO Medical Taken + Medical Scheduled + Bereavement {summaries.length > 0 ? ( summaries.map((summary) => ( - - + + {summary.user?.firstName} {summary.user?.lastName} {summary.pto_pending_days || 0} @@ -98,11 +98,17 @@ const EmployeeSummary = () => { {summary.medical_taken_days || 0} + + {summary.medical_scheduled_days || 0} + + + {summary.bereavement_taken_days || 0} + )) ) : ( - + No summaries found for {selectedYear} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 88779f1..de1230b 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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)} /> + handleReject(task.id)} + /> )) @@ -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 && (
diff --git a/frontend/src/pages/time_off_requests/time_off_requests-new.tsx b/frontend/src/pages/time_off_requests/time_off_requests-new.tsx index 2e103f3..b467bf5 100644 --- a/frontend/src/pages/time_off_requests/time_off_requests-new.tsx +++ b/frontend/src/pages/time_off_requests/time_off_requests-new.tsx @@ -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(); + // 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)} > -
- + {({ values, errors, touched }) => ( + + - {/* Requester - Only visible to Admin */} - {isAdmin && ( - - + {/* Requester - Only visible to Admin */} + {isAdmin && ( + + + + )} + + + + + + + + - )} - - - - - - - - + {/* Duration Type Radio */} + + + + + + + + + + - {/* Date Requested */} - - - + {/* Conditional Fields based on is_multiple */} + {values.is_multiple === 'multiple' ? ( +
+
+ + + + + + +
+
+ + Working days will be calculated automatically based on your schedule and holidays. +
+
+ ) : ( +
+
+ + + + + + + + + + +
+
+ )} - {/* Duration Select */} - - - - - - - + {/* Hidden Fields for Backend */} +
+ + + +
- {/* Hidden Fields for Backend */} -
- - - -
+ + + - - - - - - - - - router.push('/time_off_requests/time_off_requests-list')}/> - - + + + + + router.push('/time_off_requests/time_off_requests-list')}/> + + + )}