From 9a3496f9fdd68e0c401dc05fed070e875533dfbe Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 02:12:37 +0000 Subject: [PATCH] Autosave: 20260217-021237 --- backend/src/db/api/login_backgrounds.js | 100 ++++ .../1771300000001-add-balances-to-summary.js | 36 ++ .../1771400000000-create-login-backgrounds.js | 45 ++ backend/src/db/models/login_backgrounds.js | 51 +++ .../src/db/models/yearly_leave_summaries.js | 14 +- backend/src/index.js | 5 +- backend/src/routes/login_backgrounds.js | 34 ++ backend/src/routes/yearly_leave_summaries.js | 26 +- backend/src/services/login_backgrounds.js | 59 +++ backend/src/services/pto_journal_entries.js | 28 +- backend/src/services/users.js | 28 +- frontend/src/components/FormField.tsx | 37 +- .../configureYearly_leave_summariesCols.tsx | 26 +- frontend/src/menuAside.ts | 14 +- frontend/src/pages/dashboard.tsx | 2 +- frontend/src/pages/employee-summary.tsx | 4 +- frontend/src/pages/holidays/holidays-edit.tsx | 427 ++---------------- frontend/src/pages/holidays/holidays-new.tsx | 268 +---------- frontend/src/pages/index.tsx | 170 +------ frontend/src/pages/login.tsx | 151 ++----- frontend/src/pages/profile.tsx | 30 +- .../balance-adjustment.tsx | 104 +++++ frontend/src/pages/settings/index.tsx | 54 ++- .../src/pages/settings/login-backgrounds.tsx | 149 ++++++ .../pto_journal_entriesSlice.ts | 31 +- frontend/src/stores/styleSlice.ts | 10 +- 26 files changed, 904 insertions(+), 999 deletions(-) create mode 100644 backend/src/db/api/login_backgrounds.js create mode 100644 backend/src/db/migrations/1771300000001-add-balances-to-summary.js create mode 100644 backend/src/db/migrations/1771400000000-create-login-backgrounds.js create mode 100644 backend/src/db/models/login_backgrounds.js create mode 100644 backend/src/routes/login_backgrounds.js create mode 100644 backend/src/services/login_backgrounds.js create mode 100644 frontend/src/pages/pto_journal_entries/balance-adjustment.tsx create mode 100644 frontend/src/pages/settings/login-backgrounds.tsx diff --git a/backend/src/db/api/login_backgrounds.js b/backend/src/db/api/login_backgrounds.js new file mode 100644 index 0000000..302e8f5 --- /dev/null +++ b/backend/src/db/api/login_backgrounds.js @@ -0,0 +1,100 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class LoginBackgroundsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.login_backgrounds.create( + { + month: data.month, + importHash: data.importHash, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction } + ); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: 'login_backgrounds', + belongsToColumn: 'image', + belongsToId: record.id, + }, + data.image, + options + ); + + return record; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.login_backgrounds.findByPk(id, { transaction }); + if (!record) return null; + + const payload = { + updatedById: currentUser.id, + }; + if (data.month !== undefined) payload.month = data.month; + + await record.update(payload, { transaction }); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: 'login_backgrounds', + belongsToColumn: 'image', + belongsToId: record.id, + }, + data.image, + options + ); + + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.login_backgrounds.findOne({ where, transaction }); + if (!record) return null; + + const output = record.get({ plain: true }); + output.image = await record.getImage({ transaction }); + return output; + } + + static async findAll(filter, options) { + const transaction = (options && options.transaction) || undefined; + const include = [ + { + model: db.file, + as: 'image', + }, + ]; + + const { rows, count } = await db.login_backgrounds.findAndCountAll({ + where: {}, + include, + order: [['month', 'ASC']], + transaction, + }); + + return { rows, count }; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.login_backgrounds.findByPk(id, { transaction }); + if (!record) return null; + + await record.destroy({ transaction }); + return record; + } +}; diff --git a/backend/src/db/migrations/1771300000001-add-balances-to-summary.js b/backend/src/db/migrations/1771300000001-add-balances-to-summary.js new file mode 100644 index 0000000..6813359 --- /dev/null +++ b/backend/src/db/migrations/1771300000001-add-balances-to-summary.js @@ -0,0 +1,36 @@ + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn('yearly_leave_summaries', 'opening_balance', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + defaultValue: 0, + }, { transaction }); + + await queryInterface.addColumn('yearly_leave_summaries', 'ending_balance', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + defaultValue: 0, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('yearly_leave_summaries', 'opening_balance', { transaction }); + await queryInterface.removeColumn('yearly_leave_summaries', 'ending_balance', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1771400000000-create-login-backgrounds.js b/backend/src/db/migrations/1771400000000-create-login-backgrounds.js new file mode 100644 index 0000000..883d938 --- /dev/null +++ b/backend/src/db/migrations/1771400000000-create-login-backgrounds.js @@ -0,0 +1,45 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('login_backgrounds', { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + month: { + type: Sequelize.INTEGER, + allowNull: false, + unique: true, + comment: '0: Default, 1-12: Specific Month', + }, + importHash: { + type: Sequelize.STRING(255), + allowNull: true, + unique: true, + }, + createdAt: { + type: Sequelize.DATE, + }, + updatedAt: { + type: Sequelize.DATE, + }, + createdById: { + type: Sequelize.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + updatedById: { + type: Sequelize.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('login_backgrounds'); + }, +}; diff --git a/backend/src/db/models/login_backgrounds.js b/backend/src/db/models/login_backgrounds.js new file mode 100644 index 0000000..4a5c252 --- /dev/null +++ b/backend/src/db/models/login_backgrounds.js @@ -0,0 +1,51 @@ +module.exports = function(sequelize, DataTypes) { + const loginBackgrounds = sequelize.define( + 'login_backgrounds', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + month: { + type: DataTypes.INTEGER, + allowNull: false, + unique: true, + validate: { + min: 0, + max: 12, + }, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: false, + tableName: 'login_backgrounds', + }, + ); + + loginBackgrounds.associate = (db) => { + db.login_backgrounds.belongsTo(db.users, { + as: 'createdBy', + }); + db.login_backgrounds.belongsTo(db.users, { + as: 'updatedBy', + }); + db.login_backgrounds.hasMany(db.file, { + as: 'image', + foreignKey: 'belongsToId', + constraints: false, + scope: { + belongsTo: 'login_backgrounds', + belongsToColumn: 'image', + }, + }); + }; + + return loginBackgrounds; +}; diff --git a/backend/src/db/models/yearly_leave_summaries.js b/backend/src/db/models/yearly_leave_summaries.js index b4d3a2c..f004139 100644 --- a/backend/src/db/models/yearly_leave_summaries.js +++ b/backend/src/db/models/yearly_leave_summaries.js @@ -77,6 +77,16 @@ vacation_pay_paid_amount: { }, +opening_balance: { + type: DataTypes.DECIMAL, + allowNull: true, + }, + +ending_balance: { + type: DataTypes.DECIMAL, + allowNull: true, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -134,6 +144,4 @@ vacation_pay_paid_amount: { return yearly_leave_summaries; -}; - - +}; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 260260b..0c13254 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,3 @@ - const express = require('express'); const cors = require('cors'); const app = express(); @@ -41,6 +40,7 @@ const office_calendar_eventsRoutes = require('./routes/office_calendar_events'); const approval_tasksRoutes = require('./routes/approval_tasks'); const appSettingsRoutes = require('./routes/app_settings'); +const loginBackgroundsRoutes = require('./routes/login_backgrounds'); const checkLockout = require('./middlewares/lockout'); @@ -128,6 +128,7 @@ app.use('/api/approval_tasks', authAndLockout, approval_tasksRoutes); // App Settings (Admin only basically, but handled in route). // IMPORTANT: Do NOT apply lockout middleware here, otherwise admin can't unlock! app.use('/api/app_settings', auth, appSettingsRoutes); +app.use('/api/login_backgrounds', loginBackgroundsRoutes); app.use( '/api/openai', @@ -173,4 +174,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/backend/src/routes/login_backgrounds.js b/backend/src/routes/login_backgrounds.js new file mode 100644 index 0000000..fe4997e --- /dev/null +++ b/backend/src/routes/login_backgrounds.js @@ -0,0 +1,34 @@ +const express = require('express'); +const LoginBackgroundsService = require('../services/login_backgrounds'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const passport = require('passport'); + +// Public route to get current background +router.get('/current', wrapAsync(async (req, res) => { + const record = await LoginBackgroundsService.findCurrent(); + res.send(record); +})); + +// Admin routes +router.get('/', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + const payload = await LoginBackgroundsService.listAll(req.currentUser); + res.send(payload); +})); + +router.post('/', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + await LoginBackgroundsService.create(req.body, req.currentUser); + res.sendStatus(200); +})); + +router.put('/:id', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + await LoginBackgroundsService.update(req.params.id, req.body, req.currentUser); + res.sendStatus(200); +})); + +router.delete('/:id', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + await LoginBackgroundsService.remove(req.params.id, req.currentUser); + res.sendStatus(200); +})); + +module.exports = router; diff --git a/backend/src/routes/yearly_leave_summaries.js b/backend/src/routes/yearly_leave_summaries.js index dc8fe0a..6c71e11 100644 --- a/backend/src/routes/yearly_leave_summaries.js +++ b/backend/src/routes/yearly_leave_summaries.js @@ -308,8 +308,19 @@ router.get('/', wrapAsync(async (req, res) => { const filetype = req.query.filetype const currentUser = req.currentUser; + + let filter = { ...req.query }; + if (req.query.filter && typeof req.query.filter === 'string') { + try { + const parsed = JSON.parse(req.query.filter); + filter = { ...filter, ...parsed }; + } catch (e) { + console.error('Failed to parse filter query param:', e); + } + } + const payload = await Yearly_leave_summariesDBApi.findAll( - req.query, { currentUser } + filter, { currentUser } ); if (filetype && filetype === 'csv') { const fields = ['id', @@ -360,8 +371,19 @@ router.get('/', wrapAsync(async (req, res) => { router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; + + let filter = { ...req.query }; + if (req.query.filter && typeof req.query.filter === 'string') { + try { + const parsed = JSON.parse(req.query.filter); + filter = { ...filter, ...parsed }; + } catch (e) { + console.error('Failed to parse filter query param:', e); + } + } + const payload = await Yearly_leave_summariesDBApi.findAll( - req.query, + filter, null, { countOnly: true, currentUser } ); diff --git a/backend/src/services/login_backgrounds.js b/backend/src/services/login_backgrounds.js new file mode 100644 index 0000000..1a437c2 --- /dev/null +++ b/backend/src/services/login_backgrounds.js @@ -0,0 +1,59 @@ +const db = require('../db/models'); +const LoginBackgroundsDBApi = require('../db/api/login_backgrounds'); + +module.exports = class LoginBackgroundsService { + static async findCurrent() { + const currentMonth = new Date().getMonth() + 1; // 1-12 + + let record = await LoginBackgroundsDBApi.findBy({ month: currentMonth }); + if (!record) { + record = await LoginBackgroundsDBApi.findBy({ month: 0 }); // Default + } + + if (record && record.image && record.image.length > 0) { + return { imageUrl: record.image[0].publicUrl }; + } + + return { imageUrl: null }; + } + + static async listAll(currentUser) { + const result = await LoginBackgroundsDBApi.findAll({}, { currentUser }); + return result; + } + + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await LoginBackgroundsDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + return record; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(id, data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await LoginBackgroundsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return record; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LoginBackgroundsDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/pto_journal_entries.js b/backend/src/services/pto_journal_entries.js index 3183eaf..1aaa756 100644 --- a/backend/src/services/pto_journal_entries.js +++ b/backend/src/services/pto_journal_entries.js @@ -67,6 +67,7 @@ module.exports = class Pto_journal_entriesService { static async bulkAdjust(data, currentUser) { const { userIds, entry_type, leave_bucket, amount_hours, amount_days, memo } = data; + const calendar_year = data.calendar_year || new Date().getFullYear(); const transaction = await db.sequelize.transaction(); try { const entries = userIds.map(userId => ({ @@ -79,13 +80,38 @@ module.exports = class Pto_journal_entriesService { entered_byId: currentUser.id, entered_at: new Date(), posting_status: 'posted', - calendar_year: new Date().getFullYear(), + calendar_year, effective_at: new Date(), counts_against_balance: true, // Assuming adjustments usually count })); await db.pto_journal_entries.bulkCreate(entries, { transaction }); + // Update Yearly Leave Summaries + // We assume adjustments primarily affect 'regular_pto' available balance for now + if (leave_bucket === 'regular_pto' || !leave_bucket) { + for (const userId of userIds) { + const summary = await db.yearly_leave_summaries.findOne({ + where: { userId, calendar_year }, + transaction + }); + + if (summary) { + let adjustment = Number(amount_days || 0); + if (entry_type === 'debit_manual_adjustment') { + adjustment = -adjustment; + } + + const newBalance = Number(summary.pto_available_days || 0) + adjustment; + await summary.update({ pto_available_days: newBalance }, { transaction }); + } else { + // If summary doesn't exist, we skip for now or could create it. + // Given previous tasks, we assume summaries exist or are created on user creation. + console.warn(`Yearly leave summary not found for user ${userId} year ${calendar_year}`); + } + } + } + await transaction.commit(); } catch (error) { await transaction.rollback(); diff --git a/backend/src/services/users.js b/backend/src/services/users.js index 555d193..9e2fe0e 100644 --- a/backend/src/services/users.js +++ b/backend/src/services/users.js @@ -1,11 +1,13 @@ const db = require('../db/models'); const UsersDBApi = require('../db/api/users'); +const YearlyLeaveSummariesDBApi = require('../db/api/yearly_leave_summaries'); const processFile = require("../middlewares/upload"); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); +const moment = require('moment'); const InvitationEmail = require('./email/list/invitation'); @@ -26,7 +28,7 @@ module.exports = class UsersService { 'iam.errors.userAlreadyExists', ); } else { - await UsersDBApi.create( + const newUser = await UsersDBApi.create( {data}, { @@ -34,6 +36,22 @@ module.exports = class UsersService { transaction, }, ); + + // Create yearly leave summary for the current year + const currentYear = moment().year(); + await YearlyLeaveSummariesDBApi.create({ + calendar_year: currentYear, + user: newUser.id, + pto_available_days: data.paid_pto_per_year || 20, + pto_pending_days: 0, + pto_scheduled_days: 0, + pto_taken_days: 0, + medical_taken_days: 0, + bereavement_taken_days: 0, + time_in_lieu_available_days: 0, + vacation_pay_paid_amount: 0 + }, { transaction, currentUser }); + emailsToInvite.push(email); } } else { @@ -86,6 +104,10 @@ module.exports = class UsersService { currentUser: req.currentUser }); + // TODO: Bulk import likely also needs to create yearly summaries, + // but sticking to the request scope for single user creation first. + // If requested, I can add it here too by iterating over created users. + emailsToInvite = results.map((result) => result.email); await transaction.commit(); @@ -166,6 +188,4 @@ module.exports = class UsersService { throw error; } } -}; - - +}; \ No newline at end of file diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx index 988ac39..90fddd7 100644 --- a/frontend/src/components/FormField.tsx +++ b/frontend/src/components/FormField.tsx @@ -1,4 +1,4 @@ -import { Children, cloneElement, ReactElement, ReactNode } from 'react' +import { Children, cloneElement, ReactElement, ReactNode, isValidElement } from 'react' import BaseIcon from './BaseIcon' import { useAppSelector } from '../stores/hooks'; @@ -17,12 +17,24 @@ type Props = { websiteBg?: boolean } +const extractNameFromChildren = (children: ReactNode): string => { + const names: string[] = []; + Children.forEach(children, (child) => { + if (isValidElement(child) && (child.props as any).name) { + names.push((child.props as any).name); + } + }); + return names.join(', '); +} + const FormField = ({ icons = [], ...props }: Props) => { const childrenCount = Children.count(props.children) const bgColor = useAppSelector((state) => state.style.cardsColor); const focusRing = useAppSelector((state) => state.style.focusRingColor); const corners = useAppSelector((state) => state.style.corners); const bgWebsiteColor = useAppSelector((state) => state.style.bgLayoutColor); + const showDevInfo = useAppSelector((state) => state.style.showDevInfo); + let elementWrapperClass = '' switch (childrenCount) { @@ -43,15 +55,24 @@ const FormField = ({ icons = [], ...props }: Props) => { props.borderButtom ? `border-0 border-b ${props.diversity ? "border-gray-400" : " placeholder-white border-gray-300/10 border-white "} rounded-none focus:ring-0` : '', ].join(' '); + const devFieldName = props.labelFor || extractNameFromChildren(props.children); + return (
{props.label && ( - +
+ + {showDevInfo && devFieldName && ( + + DB: {devFieldName} + + )} +
)}
{Children.map(props.children, (child: ReactElement, index) => ( @@ -77,4 +98,4 @@ const FormField = ({ icons = [], ...props }: Props) => { ) } -export default FormField +export default FormField \ No newline at end of file diff --git a/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx b/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx index cc238c8..5518b3d 100644 --- a/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx +++ b/frontend/src/components/Yearly_leave_summaries/configureYearly_leave_summariesCols.tsx @@ -207,6 +207,30 @@ export const loadColumns = async ( }, + { + field: 'opening_balance', + headerName: 'Opening Balance', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'number', + }, + + { + field: 'ending_balance', + headerName: 'Ending Balance', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'number', + }, + { field: 'actions', type: 'actions', @@ -231,4 +255,4 @@ export const loadColumns = async ( }, }, ]; -}; +}; \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index db3aaf8..1732047 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -32,14 +32,6 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS' }, - { - href: '/holiday_calendars/holiday_calendars-list', - label: 'Holiday calendars', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCalendarStar' in icon ? icon['mdiCalendarStar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_HOLIDAY_CALENDARS' - }, { href: '/holidays/holidays-list', label: 'Holidays', @@ -64,6 +56,12 @@ const menuAside: MenuAsideItem[] = [ icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, permissions: 'READ_PTO_JOURNAL_ENTRIES' }, + { + href: '/pto_journal_entries/balance-adjustment', + label: 'Balance Adjustment', + icon: icon.mdiScaleBalance, + permissions: 'READ_PTO_JOURNAL_ENTRIES' + }, { href: '/yearly_leave_summaries/yearly_leave_summaries-list', label: 'Yearly leave summaries', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 6742e44..4a8eac1 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -83,7 +83,7 @@ const Dashboard = () => { setSelectedYear(parseInt(e.target.value))} - className="px-2 py-1 border rounded dark:bg-dark-800 dark:border-dark-700" + className="pl-2 pr-10 py-1 border rounded dark:bg-dark-800 dark:border-dark-700" > {years.map(y => ( @@ -121,4 +121,4 @@ EmployeeSummary.getLayout = function getLayout(page: ReactElement) { return {page} } -export default EmployeeSummary +export default EmployeeSummary \ No newline at end of file diff --git a/frontend/src/pages/holidays/holidays-edit.tsx b/frontend/src/pages/holidays/holidays-edit.tsx index 804d9f7..8be8c8b 100644 --- a/frontend/src/pages/holidays/holidays-edit.tsx +++ b/frontend/src/pages/holidays/holidays-edit.tsx @@ -1,9 +1,6 @@ import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' import Head from 'next/head' import React, { ReactElement, useEffect, useState } from 'react' -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import dayjs from "dayjs"; import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' @@ -28,186 +25,19 @@ import {RichTextField} from "../../components/RichTextField"; import { update, fetch } from '../../stores/holidays/holidaysSlice' import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; - +import moment from 'moment'; const EditHolidaysPage = () => { const router = useRouter() const dispatch = useAppDispatch() const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - holiday_calendar: null, - - - - - - 'name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - starts_at: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - ends_at: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + name: '', + starts_at: '', + ends_at: '', is_company_holiday: false, - - - - - - - - - - - - - - - - - notes: '', - - - - - - - - - - - - - - - - - - - - - - - - - } const [initialValues, setInitialValues] = useState(initVals) @@ -217,25 +47,33 @@ const EditHolidaysPage = () => { const { id } = router.query useEffect(() => { - dispatch(fetch({ id: id })) - }, [id]) - - useEffect(() => { - if (typeof holidays === 'object') { - setInitialValues(holidays) + if (id) { + dispatch(fetch({ id: id })) } - }, [holidays]) + }, [id, dispatch]) useEffect(() => { - if (typeof holidays === 'object') { + if (holidays && typeof holidays === 'object') { const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (holidays)[el]) + // Map fields and format dates + Object.keys(initVals).forEach(el => { + if (el === 'starts_at' || el === 'ends_at') { + newInitialVal[el] = holidays[el] ? moment(holidays[el]).format('YYYY-MM-DD') : ''; + } else { + newInitialVal[el] = holidays[el]; + } + }) setInitialValues(newInitialVal); } }, [holidays]) const handleSubmit = async (data) => { - await dispatch(update({ id: id, data })) + const payload = { + ...data, + starts_at: data.starts_at ? `${data.starts_at}T00:00:00` : null, + ends_at: data.ends_at ? `${data.ends_at}T23:59:59` : null, + }; + await dispatch(update({ id: id, data: payload })) await router.push('/holidays/holidays-list') } @@ -256,27 +94,6 @@ const EditHolidaysPage = () => { >
- - - - - - - - - - - - - - - - - - - - - { component={SelectField} options={initialValues.holiday_calendar} itemRef={'holiday_calendars'} - - - - - - - - showField={'name'} - - - - - - - - - - - - - - > - - - - - - - - - - - - @@ -330,137 +113,27 @@ const EditHolidaysPage = () => { placeholder="Name" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'starts_at': date})} + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'ends_at': date})} + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { component={SwitchField} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/pages/holidays/holidays-new.tsx b/frontend/src/pages/holidays/holidays-new.tsx index c40eec0..22b417e 100644 --- a/frontend/src/pages/holidays/holidays-new.tsx +++ b/frontend/src/pages/holidays/holidays-new.tsx @@ -28,103 +28,12 @@ import { useRouter } from 'next/router' import moment from 'moment'; const initialValues = { - - - - - - - - - - - - - holiday_calendar: '', - - - - name: '', - - - - - - - - - - - - - - - - - - - - starts_at: '', - - - - - - - - - - - - - - - ends_at: '', - - - - - - - - - - - - - - - - is_company_holiday: false, - - - - - - - - - - notes: '', - - - - - - - - - - - - - } @@ -139,7 +48,12 @@ const HolidaysNew = () => { const handleSubmit = async (data) => { - await dispatch(create(data)) + const payload = { + ...data, + starts_at: data.starts_at ? `${data.starts_at}T00:00:00` : null, + ends_at: data.ends_at ? `${data.ends_at}T23:59:59` : null, + }; + await dispatch(create(payload)) await router.push('/holidays/holidays-list') } return ( @@ -154,53 +68,21 @@ const HolidaysNew = () => { handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -210,120 +92,26 @@ const HolidaysNew = () => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -396,4 +150,4 @@ HolidaysNew.getLayout = function getLayout(page: ReactElement) { ) } -export default HolidaysNew +export default HolidaysNew \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 7d3b110..49dd63f 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,12 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; -import Head from 'next/head'; -import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; -import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; -import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('background'); - const textColor = useAppSelector((state) => state.style.linkColor); + const router = useRouter(); - const title = 'ET Vertical PTO' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( - - ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - - return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
- -
- ); -} - -Starter.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; + useEffect(() => { + router.replace('/login'); + }, [router]); + return null; +} \ No newline at end of file diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 07825ba..b4c6f73 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -1,12 +1,10 @@ - - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import BaseIcon from "../components/BaseIcon"; -import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; +import { mdiEye, mdiEyeOff } from '@mdi/js'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; import { Field, Form, Formik } from 'formik'; @@ -20,42 +18,44 @@ import { findMe, loginUser, resetAction } from '../stores/authSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import Link from 'next/link'; import {toast, ToastContainer} from "react-toastify"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' +import axios from 'axios'; export default function Login() { const router = useRouter(); const dispatch = useAppDispatch(); const textColor = useAppSelector((state) => state.style.linkColor); - const iconsColor = useAppSelector((state) => state.style.iconsColor); const notify = (type, msg) => toast(msg, { type }); - const [ illustrationImage, setIllustrationImage ] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('background'); - const [showPassword, setShowPassword] = useState(false); + + const [backgroundImage, setBackgroundImage] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( (state) => state.auth, ); - const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com', + + const initialValues = { + email: 'admin@flatlogic.com', password: 'ccc3acc2', - remember: true }) + remember: true + }; const title = 'ET Vertical PTO' - // Fetch Pexels image/video - useEffect( () => { - async function fetchData() { - const image = await getPexelsImage() - const video = await getPexelsVideo() - setIllustrationImage(image); - setIllustrationVideo(video); + // Fetch background image + useEffect(() => { + const fetchBackground = async () => { + try { + const response = await axios.get('/login_backgrounds/current'); + if (response.data && response.data.imageUrl) { + setBackgroundImage(response.data.imageUrl); + } + } catch (error) { + console.error('Failed to fetch login background:', error); } - fetchData(); + }; + fetchBackground(); }, []); + // Fetch user data useEffect(() => { if (token) { @@ -92,110 +92,23 @@ export default function Login() { await dispatch(loginUser(rest)); }; - const setLogin = (target: HTMLElement) => { - setInitialValues(prev => ({ - ...prev, - email : target.innerText.trim(), - password: target.dataset.password ?? '', - })); - }; - - const imageBlock = (image) => ( - - ) - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - return ( -
+ }}> {getPageTitle('Login')} -
- {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} - {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} +
- - -

{title}

- -
-
- -

Use{' '} - setLogin(e.target)}>admin@flatlogic.com{' / '} - ccc3acc2{' / '} - to login as Admin

-

Use setLogin(e.target)}>client@hello.com{' / '} - da2433513f4f{' / '} - to login as User

-
-
- -
-
-
- - + +

{title}

{page}; -}; +}; \ No newline at end of file diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index f5eb7cf..6985d20 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -1,6 +1,7 @@ import { mdiChartTimelineVariant, mdiUpload, + mdiDeveloperBoard } from '@mdi/js'; import Head from 'next/head'; import React, { ReactElement, useEffect, useState } from 'react'; @@ -26,6 +27,7 @@ import { SwitchField } from '../components/SwitchField'; import { SelectField } from '../components/SelectField'; import { update, fetch } from '../stores/users/usersSlice'; +import { toggleDevInfo } from '../stores/styleSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useRouter } from 'next/router'; import {findMe} from "../stores/authSlice"; @@ -34,6 +36,8 @@ const EditUsers = () => { const { currentUser, isFetching, token } = useAppSelector( (state) => state.auth, ); + const showDevInfo = useAppSelector((state) => state.style.showDevInfo); + const router = useRouter(); const dispatch = useAppDispatch(); const notify = (type, msg) => toast(msg, { type }); @@ -168,6 +172,30 @@ const EditUsers = () => {
+ + + {''} + + + +
+
+

Dev View

+

Show database field names in forms

+
+ + dispatch(toggleDevInfo())} + /> + +
+
); @@ -177,4 +205,4 @@ EditUsers.getLayout = function getLayout(page: ReactElement) { return {page}; }; -export default EditUsers; +export default EditUsers; \ No newline at end of file diff --git a/frontend/src/pages/pto_journal_entries/balance-adjustment.tsx b/frontend/src/pages/pto_journal_entries/balance-adjustment.tsx new file mode 100644 index 0000000..36e8737 --- /dev/null +++ b/frontend/src/pages/pto_journal_entries/balance-adjustment.tsx @@ -0,0 +1,104 @@ +import { mdiScaleBalance } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import { useAppDispatch } from '../../stores/hooks'; +import { bulkAdjust } from '../../stores/pto_journal_entries/pto_journal_entriesSlice'; +import BaseButton from '../../components/BaseButton'; +import FormField from '../../components/FormField'; +import { Field, Form, Formik } from 'formik'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; + +const BalanceAdjustmentPage = () => { + const dispatch = useAppDispatch(); + const currentYear = new Date().getFullYear(); + + const handleSubmit = async (values, { setSubmitting, resetForm }) => { + const payload = { + userIds: values.userIds, + entry_type: values.adjustmentType, + leave_bucket: 'regular_pto', + amount_days: values.amountDays, + memo: values.memo, + calendar_year: values.year, + }; + + await dispatch(bulkAdjust(payload)); + setSubmitting(false); + resetForm(); + }; + + return ( + <> + + {getPageTitle('Balance Adjustment')} + + + + {''} + + + + + {({ isSubmitting }) => ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + )} +
+
+
+ + ); +}; + +BalanceAdjustmentPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default BalanceAdjustmentPage; \ No newline at end of file diff --git a/frontend/src/pages/settings/index.tsx b/frontend/src/pages/settings/index.tsx index eb66609..94e2b53 100644 --- a/frontend/src/pages/settings/index.tsx +++ b/frontend/src/pages/settings/index.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, useState, useEffect } from 'react'; import Head from 'next/head'; import { Formik, Form, Field } from 'formik'; import axios from 'axios'; -import { mdiCog, mdiCheck } from '@mdi/js'; +import { mdiCog, mdiCheck, mdiCalendar, mdiImageArea } from '@mdi/js'; import CardBox from '../../components/CardBox'; import LayoutAuthenticated from '../../layouts/Authenticated'; import SectionMain from '../../components/SectionMain'; @@ -14,8 +14,10 @@ import { getPageTitle } from '../../config'; import { SelectFieldMany } from '../../components/SelectFieldMany'; import { SwitchField } from '../../components/SwitchField'; import moment from 'moment'; +import { useRouter } from 'next/router'; const SettingsPage = () => { + const router = useRouter(); const [initialValues, setInitialValues] = useState({ lockoutEnabled: false, lockoutUntil: '', @@ -43,32 +45,6 @@ const SettingsPage = () => { const handleSubmit = async (values) => { try { - // Transform allowedUserIds back to array of strings if needed - // But SelectFieldMany returns array of IDs usually? No, let's check. - // SelectFieldMany: form.setFieldValue(field.name, data.map(el => (el?.value || null))); - // So it sets array of IDs. - // Wait, initialValues allowedUserIds expects array of IDs? No, the component expects objects if using `options` or `value` logic internally? - // `field.value` in SelectFieldMany is used. - // If `field.value` is `['id1']`, `useEffect` inside `SelectFieldMany` runs: - // if (field.value?.[0] && typeof field.value[0] !== 'string') -> map to IDs. - // So if I pass objects, it maps to IDs. If I pass IDs, it's fine. - // But `options` are loaded async. - // I'll stick to passing array of IDs in initialValues. - // `SelectFieldMany` implementation is a bit weird. It sets value from `options` prop change or `field.value` change. - // If `options` is empty initially, `value` is empty. - // But `loadOptions` fetches options. - // Since `options` prop is `[]` in my usage (`options={[]}`), it relies on `loadOptions`. - // `AsyncPaginate` handles initial value label resolution if `defaultOptions` is true? No. - // Usually async select needs the full object to show the label for existing value. - // I might need to fetch the full user objects for allowedUserIds to display them correctly initially. - // I'll skip fetching full objects for now as it complicates things. It might just show IDs or empty labels until loaded? - // Actually, `SelectFieldMany` in this codebase seems designed to receive the FULL options list in `options` prop if not using `loadOptions` exclusively for search? - // No, `loadOptions={callApi}` is passed. - // The issue is displaying the initial selection. - // Without fetching the user objects (labels), the select won't show names. - // I'll leave it as is for now. The user didn't ask for perfection on the UI label loading, just the feature. - // I'll pass IDs. - const payload = { ...values, allowedUserIds: values.allowedUserIds, // Already array of IDs from SelectFieldMany @@ -149,6 +125,28 @@ const SettingsPage = () => { )}
+ + + +
+

Manage holiday calendars and other system data.

+ + router.push('/holiday_calendars/holiday_calendars-list')} + /> + router.push('/settings/login-backgrounds')} + /> + +
+
+ ); @@ -158,4 +156,4 @@ SettingsPage.getLayout = function getLayout(page: ReactElement) { return {page}; }; -export default SettingsPage; \ No newline at end of file +export default SettingsPage; diff --git a/frontend/src/pages/settings/login-backgrounds.tsx b/frontend/src/pages/settings/login-backgrounds.tsx new file mode 100644 index 0000000..3484fa2 --- /dev/null +++ b/frontend/src/pages/settings/login-backgrounds.tsx @@ -0,0 +1,149 @@ +import React, { ReactElement, useState, useEffect } from 'react'; +import Head from 'next/head'; +import { mdiImageArea, mdiCloudUpload, mdiDelete } from '@mdi/js'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import BaseButton from '../../components/BaseButton'; +import BaseButtons from '../../components/BaseButtons'; +import { getPageTitle } from '../../config'; +import axios from 'axios'; +import FileUploader from '../../components/Uploaders/UploadService'; + +const months = [ + { value: 0, label: 'Default' }, + { value: 1, label: 'January' }, + { value: 2, label: 'February' }, + { value: 3, label: 'March' }, + { value: 4, label: 'April' }, + { value: 5, label: 'May' }, + { value: 6, label: 'June' }, + { value: 7, label: 'July' }, + { value: 8, label: 'August' }, + { value: 9, label: 'September' }, + { value: 10, label: 'October' }, + { value: 11, label: 'November' }, + { value: 12, label: 'December' }, +]; + +const LoginBackgroundsPage = () => { + const [backgrounds, setBackgrounds] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchBackgrounds = async () => { + try { + const response = await axios.get('/login_backgrounds'); + setBackgrounds(response.data.rows); + } catch (error) { + console.error('Failed to fetch backgrounds:', error); + } + }; + + useEffect(() => { + fetchBackgrounds(); + }, []); + + const handleUpload = async (month, file) => { + if (!file) return; + setLoading(true); + try { + // 1. Upload file + const uploadedFile = await FileUploader.upload('login_backgrounds', file, { image: true }); + + // 2. Save record + const existing = backgrounds.find(b => b.month === month); + + if (existing) { + await axios.put(`/login_backgrounds/${existing.id}`, { month, image: [uploadedFile] }); + } else { + await axios.post('/login_backgrounds', { month, image: [uploadedFile] }); + } + + await fetchBackgrounds(); + } catch (error) { + console.error('Failed to upload:', error); + alert('Upload failed'); + } finally { + setLoading(false); + } + }; + + const handleRemove = async (id) => { + if (!confirm('Are you sure you want to remove this background?')) return; + setLoading(true); + try { + await axios.delete(`/login_backgrounds/${id}`); + await fetchBackgrounds(); + } catch (error) { + console.error('Failed to remove:', error); + } finally { + setLoading(false); + } + }; + + return ( + <> + + {getPageTitle('Login Backgrounds')} + + + + +
+ {months.map((month) => { + const bg = backgrounds.find((b) => b.month === month.value); + const imageUrl = bg?.image?.[0]?.publicUrl; + + return ( + +

{month.label}

+ +
+ {imageUrl ? ( + {month.label} + ) : ( + No Image + )} +
+ + +
+ + handleUpload(month.value, e.target.files[0])} + disabled={loading} + /> +
+ + {bg && ( + handleRemove(bg.id)} + disabled={loading} + /> + )} +
+
+ ); + })} +
+
+ + ); +}; + +LoginBackgroundsPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default LoginBackgroundsPage; diff --git a/frontend/src/stores/pto_journal_entries/pto_journal_entriesSlice.ts b/frontend/src/stores/pto_journal_entries/pto_journal_entriesSlice.ts index 9e8b58e..dc811ca 100644 --- a/frontend/src/stores/pto_journal_entries/pto_journal_entriesSlice.ts +++ b/frontend/src/stores/pto_journal_entries/pto_journal_entriesSlice.ts @@ -122,6 +122,22 @@ export const update = createAsyncThunk('pto_journal_entries/updatePto_journal_en } }) +export const bulkAdjust = createAsyncThunk('pto_journal_entries/bulkAdjust', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post( + 'pto_journal_entries/bulk-adjust', + { data } + ) + return result.data + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } +}) + export const pto_journal_entriesSlice = createSlice({ name: 'pto_journal_entries', @@ -221,6 +237,19 @@ export const pto_journal_entriesSlice = createSlice({ rejectNotify(state, action); }) + builder.addCase(bulkAdjust.pending, (state) => { + state.loading = true + resetNotify(state); + }) + builder.addCase(bulkAdjust.fulfilled, (state) => { + state.loading = false + fulfilledNotify(state, 'Balance adjustment successful'); + }) + builder.addCase(bulkAdjust.rejected, (state, action) => { + state.loading = false + rejectNotify(state, action); + }) + }, }) @@ -228,4 +257,4 @@ export const pto_journal_entriesSlice = createSlice({ // Action creators are generated for each case reducer function export const { setRefetch } = pto_journal_entriesSlice.actions -export default pto_journal_entriesSlice.reducer +export default pto_journal_entriesSlice.reducer \ No newline at end of file diff --git a/frontend/src/stores/styleSlice.ts b/frontend/src/stores/styleSlice.ts index 81b0c2c..767989c 100644 --- a/frontend/src/stores/styleSlice.ts +++ b/frontend/src/stores/styleSlice.ts @@ -28,6 +28,7 @@ interface StyleState { shadow: string; websiteSectionStyle: string; textSecondary: string; + showDevInfo: boolean; } @@ -56,6 +57,7 @@ const initialState: StyleState = { shadow: styles.white.shadow, websiteSectionStyle: styles.white.websiteSectionStyle, textSecondary: styles.white.textSecondary, + showDevInfo: false, } @@ -94,10 +96,14 @@ export const styleSlice = createSlice({ state[`${key}Style`] = style[key] } }, + + toggleDevInfo: (state) => { + state.showDevInfo = !state.showDevInfo; + } }, }) // Action creators are generated for each case reducer function -export const { setDarkMode, setStyle } = styleSlice.actions +export const { setDarkMode, setStyle, toggleDevInfo } = styleSlice.actions -export default styleSlice.reducer +export default styleSlice.reducer \ No newline at end of file