diff --git a/backend/src/db/api/pay_types.js b/backend/src/db/api/pay_types.js index c5dec32..28916b3 100644 --- a/backend/src/db/api/pay_types.js +++ b/backend/src/db/api/pay_types.js @@ -141,10 +141,10 @@ module.exports = class Pay_typesDBApi { if (data.pay_method !== undefined) updatePayload.pay_method = data.pay_method; - if (data.hourly_rate !== undefined) updatePayload.hourly_rate = data.hourly_rate; + if (data.hourly_rate !== undefined) updatePayload.hourly_rate = data.hourly_rate === "" ? null : data.hourly_rate; - if (data.commission_rate !== undefined) updatePayload.commission_rate = data.commission_rate; + if (data.commission_rate !== undefined) updatePayload.commission_rate = data.commission_rate === "" ? null : data.commission_rate; if (data.active !== undefined) updatePayload.active = data.active; diff --git a/backend/src/index.js b/backend/src/index.js index a3b889b..d748c03 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -44,6 +44,7 @@ const job_chemical_usagesRoutes = require('./routes/job_chemical_usages'); const payroll_runsRoutes = require('./routes/payroll_runs'); const payroll_line_itemsRoutes = require('./routes/payroll_line_items'); +const reportsRoutes = require("./routes/reports"); const getBaseUrl = (url) => { diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js new file mode 100644 index 0000000..b13c292 --- /dev/null +++ b/backend/src/routes/reports.js @@ -0,0 +1,35 @@ +const express = require('express'); +const router = express.Router(); +const passport = require('passport'); +const db = require('../db/models'); +const { wrapAsync } = require('../helpers'); +const { Op } = require('sequelize'); + +router.post('/payroll', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + const { startDate, endDate, employeeId } = req.body; + + const where = {}; + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) where.createdAt[Op.gte] = new Date(startDate); + if (endDate) where.createdAt[Op.lte] = new Date(endDate); + } + if (employeeId) { + where.employeeId = employeeId; + } + + const lineItems = await db.payroll_line_items.findAll({ + where, + include: [{ model: db.users, as: 'employee' }] + }); + + const summary = lineItems.reduce((acc, item) => { + acc.totalGrossPay += parseFloat(item.gross_pay || 0); + acc.totalHours += parseFloat(item.total_hours || 0); + return acc; + }, { totalGrossPay: 0, totalHours: 0 }); + + res.json({ lineItems, summary }); +})); + +module.exports = router; diff --git a/backend/src/services/job_logs.js b/backend/src/services/job_logs.js index 5ba18c8..96d6e8d 100644 --- a/backend/src/services/job_logs.js +++ b/backend/src/services/job_logs.js @@ -1,5 +1,6 @@ const db = require('../db/models'); const Job_logsDBApi = require('../db/api/job_logs'); +const CustomersDBApi = require('../db/api/customers'); const processFile = require("../middlewares/upload"); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); @@ -7,16 +8,23 @@ const axios = require('axios'); const config = require('../config'); const stream = require('stream'); - - - - module.exports = class Job_logsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { + let customerId = data.customer; + + // If customer is a string and not a UUID, try to find or create + if (typeof customerId === 'string' && customerId.length > 0 && !customerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) { + let customer = await db.customers.findOne({ where: { name: customerId }, transaction }); + if (!customer) { + customer = await CustomersDBApi.create({ name: customerId }, { currentUser, transaction }); + } + customerId = customer.id; + } + await Job_logsDBApi.create( - data, + { ...data, customer: customerId }, { currentUser, transaction, @@ -78,10 +86,21 @@ module.exports = class Job_logsService { 'job_logsNotFound', ); } + + let customerId = data.customer; + + // If customer is a string and not a UUID, try to find or create + if (typeof customerId === 'string' && customerId.length > 0 && !customerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) { + let customer = await db.customers.findOne({ where: { name: customerId }, transaction }); + if (!customer) { + customer = await CustomersDBApi.create({ name: customerId }, { currentUser, transaction }); + } + customerId = customer.id; + } const updatedJob_logs = await Job_logsDBApi.update( id, - data, + { ...data, customer: customerId }, { currentUser, transaction, @@ -131,8 +150,4 @@ module.exports = class Job_logsService { throw error; } } - - -}; - - +}; \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..a47d445 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..ecc1243 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect , useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d1c52aa..9ce49e6 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -12,11 +12,13 @@ const menuAside: MenuAsideItem[] = [ href: '/log-work', label: 'Log Work', icon: icon.mdiPencil, + permissions: 'CREATE_JOB_LOGS' }, { href: '/my-logs', label: 'My Logs', icon: icon.mdiViewList, + permissions: 'READ_JOB_LOGS' }, { @@ -108,9 +110,45 @@ const menuAside: MenuAsideItem[] = [ { href: '/payroll_line_items/payroll_line_items-list', label: 'Payroll line items', + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, + permissions: 'READ_PAYROLL_LINE_ITEMS' + }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, + permissions: 'READ_PAYROLL_LINE_ITEMS' + }, // @ts-ignore + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, + permissions: 'READ_PAYROLL_LINE_ITEMS' + }, icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, + permissions: 'READ_PAYROLL_LINE_ITEMS' + }, + permissions: 'READ_PAYROLL_LINE_ITEMS' + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, + permissions: 'READ_PAYROLL_LINE_ITEMS' + }, + }, + { + href: '/reports', + label: 'Payroll Reports', + icon: icon.mdiChartBar, permissions: 'READ_PAYROLL_LINE_ITEMS' }, { @@ -129,4 +167,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/log-work.tsx b/frontend/src/pages/log-work.tsx index 24ca416..2992b46 100644 --- a/frontend/src/pages/log-work.tsx +++ b/frontend/src/pages/log-work.tsx @@ -7,7 +7,42 @@ import SectionMain from '../components/SectionMain'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import { getPageTitle } from '../config'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import BaseButton from '../components/BaseButton'; +import { SelectField } from '../components/SelectField'; +import { useRouter } from 'next/router'; +import { create } from '../stores/job_logs/job_logsSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; + const LogWorkPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const currentUser = useAppSelector((state) => state.auth.currentUser); + + const initialValues = { + work_date: new Date().toISOString().slice(0, 16), + employee: currentUser?.id || '', + customer: '', + hours_conducted: '', + client_paid: '', + workers_comp_class: 'roof', + pay_type: '', + vehicle: '', + odometer_start: '', + odometer_end: '', + job_address: '', + status: 'submitted', + notes_to_admin: '', + }; + + const handleSubmit = async (data: any) => { + await dispatch(create(data)); + await router.push('/my-logs'); + }; + return ( <>
@@ -18,7 +53,51 @@ const LogWorkPage = () => { {''}Work log form goes here.
+List of my job logs.
-{JSON.stringify(reportData, null, 2)}
+