Compare commits

...

26 commits

Author SHA1 Message Date
3a23f60488 Merge branch 'main' into feature/settings-page 2026-02-07 12:04:11 -05:00
40db6ec877 Merge branch 'feature/settings-page' of https://github.com/Doble-Technologies/ShiftSync-Website into feature/settings-page 2025-12-01 22:55:55 -05:00
941110abc1 Settings add 2025-12-01 22:55:54 -05:00
4ba9e91eee Update 2025-08-09 15:49:15 -04:00
a0bb4bef4a Init API Side 2025-06-23 17:55:42 -04:00
16eb49d41e Merge branch 'main' into feature/settings-page 2025-06-15 22:32:05 -04:00
bb8a672b98 Merge branch 'main' into feature/settings-page 2025-06-12 16:33:18 -04:00
6389eb8d6b Merge branch 'main' into feature/settings-page 2025-06-12 16:25:53 -04:00
4e49222e2b Merge branch 'main' into feature/settings-page 2025-06-12 16:24:08 -04:00
3d274363e7 Merge branch 'main' into feature/settings-page 2025-06-12 16:23:11 -04:00
ec58d344dc Merge branch 'main' into feature/settings-page 2025-06-12 16:19:20 -04:00
e60de19e02 Merge branch 'main' into feature/settings-page 2025-06-12 16:18:22 -04:00
6806cdabfb Merge branch 'main' into feature/settings-page 2025-06-12 16:16:04 -04:00
79c3acb6be Merge branch 'main' into feature/settings-page 2025-06-12 16:12:27 -04:00
0010ba3a4f Merge branch 'main' into feature/settings-page 2025-06-12 16:09:27 -04:00
1b7d1e54e2 Merge branch 'main' into feature/settings-page 2025-06-12 16:07:48 -04:00
1d00dd528f Remove Echo 2025-06-12 15:44:30 -04:00
b0670cc3a3 Update Settings.jsx 2025-06-12 15:41:28 -04:00
f4766ed7e1 Merge branch 'main' into feature/settings-page 2025-06-12 15:41:24 -04:00
bdfe409891 Integrity 1 2025-06-11 10:53:22 -04:00
20c088e9e7 Update Dockerfile 2025-06-11 10:42:19 -04:00
375059e3ee Update Dockerfile 2025-06-11 10:39:55 -04:00
21ac649bba Merge branch 'main' into feature/settings-page 2025-06-11 10:22:36 -04:00
3a7a6d3e2c Update package.json 2025-06-11 10:22:31 -04:00
fa56c3a785 Update docker-compose.yaml 2025-06-10 23:17:33 -04:00
49c5e4026c Update docker-compose.yaml 2025-06-10 23:10:05 -04:00
13 changed files with 4654 additions and 656 deletions

6
api/router/rest/index.js Normal file
View file

@ -0,0 +1,6 @@
import express from 'express';
import { userDataRouter } from './userData/index.js';
export const restRouter = express.Router();
restRouter.use('/userData', userDataRouter);

View file

@ -0,0 +1,17 @@
import express from 'express';
import { databaseServices } from '../../../services/index.js';
export const userDataRouter = express.Router();
userDataRouter.get('/', async (req, res) => {
let data;
try {
data = await databaseServices.getUsers(req.query);
res.status(200);
} catch (err) {
data = { Error: err?.message };
res.status(500);
} finally {
res.send(data);
}
})

6
api/router/routes.js Normal file
View file

@ -0,0 +1,6 @@
import express from 'express';
import { restRouter } from './rest/index.js';
export const routes = express.Router();
routes.use('/rest', restRouter);

View file

@ -0,0 +1,27 @@
import { Pool } from 'pg';
import 'dotenv/config';
const poolCreds = {
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DB,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
ssl:
process.env.APP_ENV === 'local'
? null
: { require: true, rejectUnauthorized }
};
const pool = new Pool(poolCreds);
export const runQuery = async (query, params = []) => {
const client = await pool.connect();
try {
const pgQueryResponse = await client.query(query, params);
return pgQueryResponse?.rows || [];
} finally {
client.release();
}
}

View file

@ -0,0 +1,14 @@
import express from 'express';
const router = express.Router();
// GET /api/departments
router.get('/', (req, res) => {
res.json([{ id: 1, name: 'Fire Department' }]);
});
// POST /api/departments
router.post('/', (req, res) => {
res.status(201).json({ message: 'Department created' });
});
export default router;

View file

@ -0,0 +1,15 @@
import { runQuery } from '../connection.js';
const getUsers = async (args) => {
try {
const dataResp = await runQuery('SELECT * FROM users');
return dataResp;
} catch (err) {
console.log('GET USERS ERROR: ', err);
throw err;
}
};
export const userDataOperations = {
getUsers
}

View file

@ -0,0 +1,16 @@
import express from 'express';
const router = express.Router();
// GET /api/users
router.get('/', (req, res) => {
res.json([{ id: 1, name: 'John Doe' }]);
});
// POST /api/users
router.post('/', (req, res) => {
res.status(201).json({ message: 'User created' });
});
// Add more routes like PUT, DELETE here later
export default router;

View file

@ -194,7 +194,8 @@ export const TransferBox = ({ fields, leftGroup, rightGroup, onSave, user={} })
</CenterButton> </CenterButton>
<CenterButton <CenterButton
onClick={() => { onClick={() => {
return onSave(rightItems?.map((item) => { return item?.id })) onSave(rightItems?.map((item) => { return item?.id }));
setItemSelected({});
}} }}
color='#00B33C' color='#00B33C'
buttonEnabled={hasChanges} buttonEnabled={hasChanges}

View file

@ -5,4 +5,6 @@ export const useLocalStore = create((set) => ({
setUser: (user) => set({ user }), setUser: (user) => set({ user }),
department: null, department: null,
setDepartment: (department) => set({ department }), setDepartment: (department) => set({ department }),
access: null,
setAccess: (access) => set({ access }),
})); }));

3856
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,492 +1,496 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Stack } from '@mui/material'; import { Stack } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import EditOffIcon from '@mui/icons-material/EditOff'; import EditOffIcon from '@mui/icons-material/EditOff';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxIcon from '@mui/icons-material/CheckBox';
import { ToggleTabs, TransferBox, useLocalStore } from '@components'; import { ToggleTabs, TransferBox, useLocalStore } from '@components';
import { settingsFields } from './helpers'; import { settingsFields } from './helpers';
const OuterContainer = styled(Stack)` const OuterContainer = styled(Stack)`
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
padding: 10px 10px 0px 0px; padding: 10px 10px 0px 0px;
`; `;
const Border = styled('div')` const Border = styled('div')`
height: 1px; height: 1px;
width: 100%; width: 100%;
background-color: #A8A8A8; background-color: #A8A8A8;
`; `;
const Tab = styled('div')` const Tab = styled('div')`
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
height: 100%; height: 100%;
padding: 6px 16px; padding: 6px 16px;
font-size: 20px; font-size: 20px;
font-weight: 800; font-weight: 800;
color: grey; color: grey;
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
font-family: "WDXL Lubrifont TC"; font-family: "WDXL Lubrifont TC";
letter-spacing: .05em; letter-spacing: .05em;
word-spacing: .2em; word-spacing: .2em;
`; `;
const SlidingIndicator = styled('div')` const SlidingIndicator = styled('div')`
position: absolute; position: absolute;
height: 1px; height: 1px;
bottom: -1px; bottom: -1px;
background-color: #4D79FF; background-color: #4D79FF;
transition: left 0.3s ease, width 0.3s ease; transition: left 0.3s ease, width 0.3s ease;
`; `;
const CardShell = styled('div')` const CardShell = styled('div')`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
`; `;
const Card = styled('div')` const Card = styled('div')`
background-color: #C7C7C7; background-color: #C7C7C7;
width: 50%; width: 50%;
min-width: 750px; min-width: 750px;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
margin: 5px; margin: 5px;
`; `;
const CardTitle = styled('p')` const CardTitle = styled('p')`
color: white; color: white;
font-weight: 400; font-weight: 400;
font-size: 20px; font-size: 20px;
font-family: "WDXL Lubrifont TC"; font-family: "WDXL Lubrifont TC";
padding-bottom: 5px; padding-bottom: 5px;
`; `;
const CardHeader = styled('div')` const CardHeader = styled('div')`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-direction: row; flex-direction: row;
padding: 10px; padding: 10px;
`; `;
const EditArea = styled('div')` const EditArea = styled('div')`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
padding-top: 10px; padding-top: 10px;
color: #4D79FF; color: #4D79FF;
font-weight: 600; font-weight: 600;
`; `;
const EditTextButton = styled('div')` const EditTextButton = styled('div')`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
gap: 4px; gap: 4px;
padding-bottom: 2px; padding-bottom: 2px;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: border-bottom 0.2s ease; transition: border-bottom 0.2s ease;
&:hover { &:hover {
border-bottom: 2px solid #4D79FF; border-bottom: 2px solid #4D79FF;
} }
`; `;
const CardBody = styled('div')` const CardBody = styled('div')`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
`; `;
const InnerCard = styled('div')` const InnerCard = styled('div')`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 10px;
width: 50%; width: 50%;
${({ centered }) => centered && ` ${({ centered }) => centered && `
align-items: center; align-items: center;
`} `}
`; `;
const InnerCardRow = styled('div')` const InnerCardRow = styled('div')`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 5px; padding: 5px;
`; `;
const InnerCardRowLabel = styled('label')` const InnerCardRowLabel = styled('label')`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
text-align: right; text-align: right;
width: 60%; width: 60%;
padding-right: 10px; padding-right: 10px;
font-size: 14px; font-size: 14px;
`; align-items: center;
`;
const InnerCardRowInput = styled('input')`
display: flex; const InnerCardRowInput = styled('input')`
justify-content: flex-end; display: flex;
width: 100%; justify-content: flex-end;
padding: 4px; width: 100%;
`; padding: 4px;
`;
const InnerCardRadioInput = styled('input')`
padding-right: 5px; const InnerCardRadioInput = styled('input')`
`; padding-right: 5px;
`;
const InnerCardRadioLabel = styled('label')`
font-size: 14px; const InnerCardRadioLabel = styled('label')`
padding-left: 5px; font-size: 14px;
`; padding-left: 5px;
align-items: center;
const InnerCardRowRadioDiv = styled('div')` `;
display: flex;
justify-content: space-evenly; const InnerCardRowRadioDiv = styled('div')`
width: 100%; display: flex;
padding: 4px; justify-content: space-evenly;
`; width: 100%;
padding: 4px;
const FormRadioButtonLabel = styled('label')``; `;
const FormInputButtonDiv = styled('div')` const FormRadioButtonLabel = styled('label')``;
display: flex;
flex-direction: row; const FormInputButtonDiv = styled('div')`
justify-content: flex-end; display: flex;
padding-top: 10px; flex-direction: row;
`; justify-content: flex-end;
padding-top: 10px;
const FormInputButton = styled('input')` `;
padding: 4px 10px;
border-radius: 4px; const FormInputButton = styled('input')`
`; padding: 4px 10px;
border-radius: 4px;
export const Settings = () => { `;
const { user, department, setDepartment } = useLocalStore();
export const Settings = () => {
const isAdministrator = user?.administrator; const { user, department, setDepartment } = useLocalStore();
const isManager = user?.manager;
const isScheduler = user?.scheduler; const isAdministrator = user?.administrator;
const isManager = user?.manager;
const originalDepartmentRef = useRef(department); const isScheduler = user?.scheduler;
const [formValues, setFormValues] = useState(() => {
const initial = {}; const originalDepartmentRef = useRef(department);
settingsFields.forEach(section => { const [formValues, setFormValues] = useState(() => {
section.cards.forEach(card => { const initial = {};
card.fields.forEach(field => { settingsFields.forEach(section => {
initial[field.id] = department?.[field.id] || ''; section.cards.forEach(card => {
}); card.fields.forEach(field => {
}); initial[field.id] = department?.[field.id] || '';
}); });
return initial; });
}); });
const pageRefs = useRef({}); return initial;
const tabs = [ });
{ const pageRefs = useRef({});
label: 'Personal', const tabs = [
value: 'personal' {
}, label: 'Personal',
{ value: 'personal'
label: 'Department', },
value: 'department' {
} label: 'Department',
]; value: 'department'
}
const [windowWidth, setWindowWidth] = useState(window.innerWidth); ];
const [editMode, setEditMode] = useState(null);
const [tabValue, setTabValue] = useState(tabs[0]); const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [pageValue, setPageValue] = useState({}); const [editMode, setEditMode] = useState(null);
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 }); const [tabValue, setTabValue] = useState(tabs[0]);
const [pageValue, setPageValue] = useState({});
const getChangedFields = (original, current) => { const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
const changed = {};
for (const key in current) { const getChangedFields = (original, current) => {
if (current[key] !== original[key]) { const changed = {};
changed[key] = current[key]; for (const key in current) {
} if (current[key] !== original[key]) {
} changed[key] = current[key];
return changed; }
}; }
return changed;
const changedFields = getChangedFields(originalDepartmentRef.current, formValues); };
const hasChanges = Object.keys(changedFields).length > 0;
const changedFields = getChangedFields(originalDepartmentRef.current, formValues);
const onSubmit = (data) => { const hasChanges = Object.keys(changedFields).length > 0;
setDepartment({
...department, const onSubmit = (data) => {
...data setDepartment({
}); ...department,
originalDepartmentRef.current = { ...data
...originalDepartmentRef.current, });
...data originalDepartmentRef.current = {
}; ...originalDepartmentRef.current,
setEditMode(null); ...data
}; };
setEditMode(null);
const formatPhoneNumber = (value) => { };
const cleaned = value.replace(/\D/g, '').slice(0, 10); // Only digits, max 10
const length = cleaned.length; const formatPhoneNumber = (value) => {
const cleaned = value.replace(/\D/g, '').slice(0, 10); // Only digits, max 10
if (length === 0) return ''; const length = cleaned.length;
if (length < 4) return `(${cleaned}`;
if (length < 7) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`; if (length === 0) return '';
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`; if (length < 4) return `(${cleaned}`;
}; if (length < 7) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
};
const handleChange = (e, type) => {
let { name, value } = e.target;
if (type === 'phone') { const handleChange = (e, type) => {
value = formatPhoneNumber(value); let { name, value } = e.target;
} if (type === 'phone') {
setFormValues(prev => ({ ...prev, [name]: value })); value = formatPhoneNumber(value);
}; }
setFormValues(prev => ({ ...prev, [name]: value }));
useEffect(() => { };
document.title = 'ShiftSync | Settings';
}, []); useEffect(() => {
document.title = 'ShiftSync | Settings';
useEffect(() => { }, []);
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize); useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
// Initial trigger (in case something else needs it) window.addEventListener('resize', handleResize);
handleResize();
// Initial trigger (in case something else needs it)
return () => { handleResize();
window.removeEventListener('resize', handleResize);
}; return () => {
}, []); window.removeEventListener('resize', handleResize);
};
useEffect(() => { }, []);
if (pageValue?.id && pageRefs.current[pageValue.id]) {
const el = pageRefs.current[pageValue.id]; useEffect(() => {
const rect = el.getBoundingClientRect(); if (pageValue?.id && pageRefs.current[pageValue.id]) {
const containerRect = el.parentNode.getBoundingClientRect(); const el = pageRefs.current[pageValue.id];
setIndicatorStyle({ const rect = el.getBoundingClientRect();
left: rect.left - containerRect.left, const containerRect = el.parentNode.getBoundingClientRect();
width: rect.width setIndicatorStyle({
}); left: rect.left - containerRect.left,
} width: rect.width
}, [pageValue?.id, windowWidth, window.innerHeight]); });
}
useEffect(() => { }, [pageValue?.id, windowWidth, window.innerHeight]);
const filteredFields = settingsFields?.filter((field) => {
const hasAccess = useEffect(() => {
(field?.accessRequired === 'administrator' && isAdministrator) || const filteredFields = settingsFields?.filter((field) => {
(field?.accessRequired === 'manager' && isManager) || const hasAccess =
(field?.accessRequired === 'scheduler' && isScheduler) || (field?.accessRequired === 'administrator' && isAdministrator) ||
(!field?.accessRequired || field?.accessRequired === 'user'); (field?.accessRequired === 'manager' && isManager) ||
(field?.accessRequired === 'scheduler' && isScheduler) ||
return field?.tab === tabValue?.value && hasAccess; (!field?.accessRequired || field?.accessRequired === 'user');
});
return field?.tab === tabValue?.value && hasAccess;
if (filteredFields.length > 0) { });
setPageValue(filteredFields[0]);
} else { if (filteredFields.length > 0) {
setPageValue(null); setPageValue(filteredFields[0]);
} } else {
}, [tabValue, isAdministrator, isManager, isScheduler]); setPageValue(null);
}
return ( }, [tabValue, isAdministrator, isManager, isScheduler]);
<div>
{user?.administrator || user?.manager ? ( return (
<OuterContainer> <div>
<ToggleTabs {user?.administrator || user?.manager ? (
tabs={tabs} <OuterContainer>
tabValue={tabValue} <ToggleTabs
setTabValue={setTabValue} tabs={tabs}
tabColor='#4D79FF' tabValue={tabValue}
/> setTabValue={setTabValue}
</OuterContainer> tabColor='#4D79FF'
) : null } />
<OuterContainer> </OuterContainer>
{settingsFields?.filter((field) => field?.tab === tabValue?.value)?.filter((field) => { ) : null }
return ((field?.accessRequired === 'administrator' && isAdministrator) || <OuterContainer>
(field?.accessRequired === 'manager' && isManager) || {settingsFields?.filter((field) => field?.tab === tabValue?.value)?.filter((field) => {
(field?.accessRequired === 'scheduler' && isScheduler) || return ((field?.accessRequired === 'administrator' && isAdministrator) ||
(field?.accessRequired === 'user' || !field?.accessRequired)) (field?.accessRequired === 'manager' && isManager) ||
})?.map((field) => { (field?.accessRequired === 'scheduler' && isScheduler) ||
return ( (field?.accessRequired === 'user' || !field?.accessRequired))
<Tab })?.map((field) => {
key={field?.id} return (
ref={(el) => { pageRefs.current[field?.id] = el; }} <Tab
onClick={() => setPageValue(field)} key={field?.id}
> ref={(el) => { pageRefs.current[field?.id] = el; }}
{field?.title} onClick={() => setPageValue(field)}
</Tab> >
) {field?.title}
})} </Tab>
<SlidingIndicator style={indicatorStyle} /> )
</OuterContainer> })}
<Border /> <SlidingIndicator style={indicatorStyle} />
<CardShell> </OuterContainer>
{pageValue?.cards?.map((card) => { <Border />
if ( <CardShell>
(card?.accessRequired === 'administrator' && isAdministrator) || {pageValue?.cards?.map((card) => {
(card?.accessRequired === 'manager' && isManager) || if (
(card?.accessRequired === 'scheduler' && isScheduler) (card?.accessRequired === 'administrator' && isAdministrator) ||
) { (card?.accessRequired === 'manager' && isManager) ||
return ( (card?.accessRequired === 'scheduler' && isScheduler)
<Card ) {
key={`${card?.id}-card`} return (
> <Card
<CardTitle key={`${card?.id}-card`}
key={`${card?.id}-card-title`} >
> <CardTitle
{card?.label} key={`${card?.id}-card-title`}
</CardTitle> >
{card?.fields?.find((field) => field?.type === 'header') !== undefined && ( {card?.label}
<div> </CardTitle>
<Border key={`${card?.id}-border-1`}/> {card?.fields?.find((field) => field?.type === 'header') !== undefined && (
<CardHeader <div>
key={`${card?.id}-card-header`} <Border key={`${card?.id}-border-1`}/>
> <CardHeader
{card?.fields?.map((field) => { key={`${card?.id}-card-header`}
return field?.type === 'header' && ( >
<InnerCardRow key={`${field?.id}-row`}> {card?.fields?.map((field) => {
<p style={{ fontSize: 14 }}> return field?.type === 'header' && (
{field?.label}: <InnerCardRow key={`${field?.id}-row`}>
</p> <p style={{ fontSize: 14 }}>
<p style={{ paddingLeft: 10, fontSize: 14 }}> {field?.label}:
{department[field?.id]} </p>
</p> <p style={{ paddingLeft: 10, fontSize: 14 }}>
</InnerCardRow> {department[field?.id]}
) </p>
})} </InnerCardRow>
</CardHeader> )
</div> })}
)} </CardHeader>
<Border key={`${card?.id}-border-2`}/> </div>
<form )}
onSubmit={(e) => { <Border key={`${card?.id}-border-2`}/>
e.preventDefault(); <form
onSubmit(changedFields); onSubmit={(e) => {
}} e.preventDefault();
> onSubmit(changedFields);
<EditArea> }}
{!card?.removeEdit && ( >
editMode === card?.id ? ( <EditArea>
<EditTextButton onClick={() => setEditMode(null)}> {!card?.removeEdit && (
<EditOffIcon sx={{ fontSize: 20 }} /> editMode === card?.id ? (
<p>View</p> <EditTextButton onClick={() => setEditMode(null)}>
</EditTextButton> <EditOffIcon sx={{ fontSize: 20 }} />
) : ( <p>View</p>
<EditTextButton onClick={() => setEditMode(card?.id)}> </EditTextButton>
<EditIcon sx={{ fontSize: 20 }} /> ) : (
<p>Edit</p> <EditTextButton onClick={() => setEditMode(card?.id)}>
</EditTextButton> <EditIcon sx={{ fontSize: 20 }} />
) <p>Edit</p>
)} </EditTextButton>
</EditArea> )
<CardBody )}
key={`${card?.id}-card-body`} </EditArea>
> <CardBody
<InnerCard key={`${card?.id}-card-body`}
key={`${card?.id}-inner-card`} >
centered={card.fields.some(f => f.type === 'transfer')} <InnerCard
> key={`${card?.id}-inner-card`}
{card?.fields?.map((field) => { centered={card.fields.some(f => f.type === 'transfer')}
let fieldType; >
if (field?.type === 'text') { {card?.fields?.map((field) => {
fieldType = <InnerCardRowInput let fieldType;
name={field?.id} if (field?.type === 'text') {
disabled={card?.id !== editMode || field?.readOnly} fieldType = <InnerCardRowInput
value={formValues[field?.id] ?? 'Not Provided'} name={field?.id}
onChange={(e) => handleChange(e, 'text')} disabled={card?.id !== editMode || field?.readOnly}
/>; value={formValues[field?.id] ?? 'Not Provided'}
} else if (field?.type === 'phone') { onChange={(e) => handleChange(e, 'text')}
fieldType = <InnerCardRowInput />;
type="tel" } else if (field?.type === 'phone') {
name="phone" fieldType = <InnerCardRowInput
placeholder="(123) 456-7890" type="tel"
value={formValues[field?.id]} name="phone"
disabled={card?.id !== editMode || field?.readOnly} placeholder="(123) 456-7890"
onChange={(e) => handleChange(e, 'phone')} value={formValues[field?.id]}
pattern="\(\d{3}\) \d{3}-\d{4}" disabled={card?.id !== editMode || field?.readOnly}
maxlength="14" onChange={(e) => handleChange(e, 'phone')}
/> pattern="\(\d{3}\) \d{3}-\d{4}"
} else if (field?.type === 'select') { maxlength="14"
fieldType = ( />
<InnerCardRowRadioDiv> } else if (field?.type === 'select') {
{field?.options?.map((option) => { fieldType = (
const inputId = `${field.id}-${option.value}`; <InnerCardRowRadioDiv>
return ( {field?.options?.map((option) => {
<FormRadioButtonLabel htmlFor={inputId} key={inputId}> const inputId = `${field.id}-${option.value}`;
<InnerCardRadioInput return (
type="radio" <FormRadioButtonLabel htmlFor={inputId} key={inputId}>
id={inputId} <InnerCardRadioInput
name={field?.id} type="radio"
value={option?.value} id={inputId}
checked={formValues[field.id] === option.value} name={field?.id}
disabled={card?.id !== editMode || field?.readOnly} value={option?.value}
onChange={(e) => handleChange(e, 'text')} checked={formValues[field.id] === option.value}
/> disabled={card?.id !== editMode || field?.readOnly}
<InnerCardRadioLabel htmlFor={inputId} key={inputId}> onChange={(e) => handleChange(e, 'text')}
{option?.label} />
</InnerCardRadioLabel> <InnerCardRadioLabel htmlFor={inputId} key={inputId}>
</FormRadioButtonLabel> {option?.label}
) </InnerCardRadioLabel>
})} </FormRadioButtonLabel>
</InnerCardRowRadioDiv> )
) })}
} else if (field?.type === 'transfer') { </InnerCardRowRadioDiv>
fieldType = <TransferBox )
style={{ display: 'flex', flexDirection: 'row' }} } else if (field?.type === 'transfer') {
user={user} fieldType = <TransferBox
fields={field} style={{ display: 'flex', flexDirection: 'row' }}
leftGroup={department[field?.origList]} user={user}
rightGroup={department[field?.id]} fields={field}
onSave={(t) => console.log(t)} leftGroup={department[field?.origList]}
/> rightGroup={department[field?.id]}
} onSave={(t) => {
return field?.type !== 'header' && ( return onSubmit({ [field.id]: t });
<InnerCardRow key={`${field?.id}-row`}> }}
{field?.label ? ( />
<InnerCardRowLabel> }
{field?.label}: return field?.type !== 'header' && (
</InnerCardRowLabel> <InnerCardRow key={`${field?.id}-row`}>
) : null } {field?.label ? (
{fieldType} <InnerCardRowLabel>
</InnerCardRow> {field?.label}:
) </InnerCardRowLabel>
})} ) : null }
</InnerCard> {fieldType}
</CardBody> </InnerCardRow>
{card?.id === editMode && ( )
<FormInputButtonDiv> })}
<FormInputButton </InnerCard>
type="submit" </CardBody>
value="Save" {card?.id === editMode && (
disabled={!hasChanges} <FormInputButtonDiv>
style={{ <FormInputButton
backgroundColor: hasChanges ? '#4D79FF' : '', type="submit"
color: hasChanges ? 'white' : '' value="Save"
}} disabled={!hasChanges}
/> style={{
</FormInputButtonDiv> backgroundColor: hasChanges ? '#4D79FF' : '',
)} color: hasChanges ? 'white' : ''
</form> }}
</Card> />
) </FormInputButtonDiv>
} )}
return null; </form>
})} </Card>
</CardShell> )
</div> }
); return null;
}; })}
</CardShell>
</div>
);
};

View file

@ -1,163 +1,170 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { Home, Profile, Schedule, Settings } from '@src/pages'; import { Home, Profile, Schedule, Settings } from '@src/pages';
import { Shell } from '@components'; import { Shell } from '@components';
import { useLocalStore } from '@components'; import { useLocalStore } from '@components';
import axios from "axios"; import { fetchAPI } from './axios.js';
const dept = { const dept = {
id: 1, id: 1,
company: 'Darien EMS - Post 53', company: 'Darien EMS - Post 53',
abv: 'DEMS', abv: 'DEMS',
billing_address: '0 Ledge Road', billing_address: '0 Ledge Road',
town: 'Darien', town: 'Darien',
state: 'Connecticut', state: 'Connecticut',
postal: '06820', postal: '06820',
country: 'United States', country: 'United States',
phone: '', phone: '',
display_time: '12', display_time: '12',
start_day: 'sunday', start_day: 'sunday',
company_logo: '', company_logo: '',
employee_count: 1, employee_count: 1,
subscription_expiration: '10/01/2025', subscription_expiration: '10/01/2025',
schedulers: [3], schedulers: [3],
managers: [2], managers: [2],
administrators: [1] administrators: [1]
}; };
const users = [ const users = [
{ {
id: 1, id: 1,
first_name: 'ShiftSync-Administrator', first_name: 'ShiftSync-Administrator',
last_name: 'Test-User', last_name: 'Test-User',
email: 'testuserA@shift-sync.com', email: 'testuserA@shift-sync.com',
is_ss_admin: false accessLevel: 150,
}, is_ss_admin: false
{ },
id: 2, {
first_name: 'ShiftSync-Manager', id: 2,
last_name: 'Test-User', first_name: 'ShiftSync-Manager',
email: 'testuserM@shift-sync.com', last_name: 'Test-User',
is_ss_admin: false email: 'testuserM@shift-sync.com',
}, accessLevel: 100,
{ is_ss_admin: false
id: 3, },
first_name: 'ShiftSync-Scheduler', {
last_name: 'Test-User', id: 3,
email: 'testuserS@shift-sync.com', first_name: 'ShiftSync-Scheduler',
is_ss_admin: false last_name: 'Test-User',
}, email: 'testuserS@shift-sync.com',
{ accessLevel: 50,
id: 4, is_ss_admin: false
first_name: 'ShiftSync-User', },
last_name: 'Test-User', {
email: 'testuserU@shift-sync.com', id: 4,
is_ss_admin: false first_name: 'ShiftSync-User',
}, last_name: 'Test-User',
] email: 'testuserU@shift-sync.com',
accessLevel: 1,
const AppRouter = () => { is_ss_admin: false
const { user, setUser, setDepartment } = useLocalStore(); },
const [userChanged, setUserChanged] = useState(false); ];
const isDev = true; // change for it.
const AppRouter = () => {
const fetchAPI = async () => { const { user, setUser, setDepartment } = useLocalStore();
const location = window.location; const [userChanged, setUserChanged] = useState(false);
const uri = `${location?.protocol}//${location.hostname}/api`;
const response = await axios.get(uri); useEffect(() => {
console.log(response.data.fruits); const init = async () => {
}; const localVersion = localStorage.getItem("APP_VERSION");
const currentVersion = window.APP_VERSION;
useEffect(() => {
const localVersion = localStorage.getItem("APP_VERSION"); if (localVersion && localVersion !== currentVersion) {
const currentVersion = window.APP_VERSION; console.log("Version changed, forcing reload");
localStorage.setItem("APP_VERSION", currentVersion);
if (localVersion && localVersion !== currentVersion) { window.location.reload(true);
console.log("Version changed, forcing reload"); return;
localStorage.setItem("APP_VERSION", currentVersion); } else {
window.location.reload(true); // force full page reload localStorage.setItem("APP_VERSION", currentVersion);
} else { }
localStorage.setItem("APP_VERSION", currentVersion);
} const data = await fetchAPI('userData', 'get');
console.log('data:', data);
fetchAPI();
// await call for getting the count of employees and any other calls to db. // TODO: Replace this with real data from your API
const employee_count = 1; // const users = data?.users || []; // Example fix
const subs_expiration = '10/22/2025'; // const dept = data?.dept || {}; // Example fix
setUser({
...users[0], const employee_count = 1;
scheduler: dept?.schedulers?.includes(1), const subs_expiration = '10/22/2025';
manager: dept?.managers?.includes(1),
administrator: dept?.administrators?.includes(1) setUser({
}); ...users[0],
const newAdministrators = dept?.administrators?.map((admin) => { scheduler: dept?.schedulers?.includes(1),
const user = users?.find((user) => { manager: dept?.managers?.includes(1),
return user?.id === admin; administrator: dept?.administrators?.includes(1)
}); });
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
}); const newAdministrators = dept?.administrators?.map((admin) => {
const newManagers = dept?.managers?.map((manager) => { const user = users?.find((user) => user?.id === admin);
const user = users?.find((user) => { return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
return user?.id === manager; });
});
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; const newManagers = dept?.managers?.map((manager) => {
}); const user = users?.find((user) => user?.id === manager);
const newSchedulers = dept?.schedulers?.map((scheduler) => { return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
const user = users?.find((user) => { });
return user?.id === scheduler;
}); const newSchedulers = dept?.schedulers?.map((scheduler) => {
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; const user = users?.find((user) => user?.id === scheduler);
}); return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
const newUsers = users?.map((user) => { });
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
}); const newUsers = users?.map((user) => ({
setDepartment({ id: user?.id,
...dept, value: `${user?.last_name}, ${user?.first_name}`
users: newUsers, }));
schedulers: newSchedulers,
managers: newManagers, setDepartment({
administrators: newAdministrators, ...dept,
employee_count, users: newUsers,
subs_expiration schedulers: newSchedulers,
}); managers: newManagers,
}, []); administrators: newAdministrators,
employee_count,
useEffect(() => { subs_expiration
if (!userChanged && user) { });
if (user?.is_ss_admin) { };
setUser({
...user, init();
scheduler: true, }, []);
manager: true,
administrator: true, useEffect(() => {
}); if (!userChanged && user) {
} else if (user?.administrator) { if (user?.is_ss_admin) {
setUser({ setUser({
...user, ...user,
scheduler: true, scheduler: true,
manager: true, manager: true,
}); administrator: true,
} else if (user?.manager) { });
setUser({ } else if (user?.administrator) {
...user, setUser({
scheduler: true, ...user,
}); scheduler: true,
} manager: true,
setUserChanged(true); });
} } else if (user?.manager) {
}, [user]); setUser({
...user,
scheduler: true,
return ( });
<Shell> }
<Routes> setUserChanged(true);
<Route path="/" element={<Home />} /> }
<Route path="/schedule" element={<Schedule />} /> }, [user]);
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes> return (
</Shell> <Shell>
); <Routes>
}; <Route path="/" element={<Home />} />
<Route path="/schedule" element={<Schedule />} />
export default AppRouter <Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Shell>
);
};
export default AppRouter

27
web/src/router/axios.js Normal file
View file

@ -0,0 +1,27 @@
import axios from 'axios';
export const fetchAPI = async (endpoint, type, body = null) => {
const location = window.location;
const url = `${location?.protocol}//${location.hostname}${location?.hostname === 'localhost' ? ':5172' : ''}/api/rest/${endpoint}`;
const requestOptions = {
headers: {
'Content-Type': 'application/json'
}
};
try {
let response;
if (type === 'post') {
response = await axios.post(url, body, requestOptions);
} else if (type === 'get') {
response = await axios.get(url, requestOptions);
} else {
console.warn('No proper type given: ', type);
return;
}
return response.data;
} catch (err) {
console.error('PG Rest Query Error: ', err);
}
};