Init API Side

This commit is contained in:
Matt DiMeglio 2025-06-23 17:55:42 -04:00
parent 16eb49d41e
commit a0bb4bef4a
18 changed files with 319 additions and 39 deletions

2
.gitignore vendored
View file

@ -12,6 +12,8 @@ dist
dist-ssr dist-ssr
*.local *.local
.env
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json

149
api/package-lock.json generated
View file

@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0" "express": "^5.1.0",
"pg": "^8.16.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
@ -831,6 +832,95 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/pg": {
"version": "8.16.2",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz",
"integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.2",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.6"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.6.tgz",
"integrity": "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.2.tgz",
"integrity": "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -844,6 +934,45 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1099,6 +1228,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -1197,6 +1335,15 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
} }
} }
} }

View file

@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0" "express": "^5.1.0",
"pg": "^8.16.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"

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

@ -2,7 +2,7 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
import { postgresServices } from './services/postgres/postgresServices.js'; import { routes } from './router/routes.js';
const app = express(); const app = express();
@ -15,9 +15,11 @@ const corsOptions = {
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use(express.json({ limit: '10mb' }));
app.use('/api', routes);
app.get("/api", (req, res) => { app.get('*route', (req, res) => {
res.json({ fruits: ["apple", "orange", "banana"] }); res.send("Hello from ShiftSync");
}); });
app.listen(5172, () => { app.listen(5172, () => {

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();
}
}

7
api/services/index.js Normal file
View file

@ -0,0 +1,7 @@
import {
userDataOperations
} from './operations/userData.js';
export const databaseServices = {
...userDataOperations
}

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

@ -1,6 +1,12 @@
export const postgresServices = { export const postgresServices = {
getUsers: async (args) => { getUsers: async (args) => {
try {
const dataResp = await runQuery('SELECT * FROM users');
return dataResp;
} catch (err) {
console.log('GET USERS ERROR: ', err);
throw err;
}
}, },
getDepartments: async (args) => { getDepartments: async (args) => {

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 }),
})); }));

8
web/package-lock.json generated
View file

@ -12,7 +12,7 @@
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.0", "@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"axios": "^1.9.0", "axios": "^1.10.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
@ -1905,9 +1905,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.9.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View file

@ -16,7 +16,7 @@
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.0", "@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"axios": "^1.9.0", "axios": "^1.10.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",

View file

@ -228,6 +228,7 @@ export const Settings = () => {
const hasChanges = Object.keys(changedFields).length > 0; const hasChanges = Object.keys(changedFields).length > 0;
const onSubmit = (data) => { const onSubmit = (data) => {
console.log('data: ', data);
setDepartment({ setDepartment({
...department, ...department,
...data ...data
@ -304,6 +305,10 @@ export const Settings = () => {
} }
}, [tabValue, isAdministrator, isManager, isScheduler]); }, [tabValue, isAdministrator, isManager, isScheduler]);
useEffect(() => {
}, [department]);
return ( return (
<div> <div>
{user?.administrator || user?.manager ? ( {user?.administrator || user?.manager ? (
@ -453,7 +458,9 @@ export const Settings = () => {
fields={field} fields={field}
leftGroup={department[field?.origList]} leftGroup={department[field?.origList]}
rightGroup={department[field?.id]} rightGroup={department[field?.id]}
onSave={(t) => console.log(t)} onSave={(t) => {
return onSubmit({ [field.id]: t });
}}
/> />
} }
return field?.type !== 'header' && ( return field?.type !== 'header' && (

View file

@ -3,7 +3,7 @@ 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,
@ -31,6 +31,7 @@ const users = [
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',
accessLevel: 150,
is_ss_admin: false is_ss_admin: false
}, },
{ {
@ -38,6 +39,7 @@ const users = [
first_name: 'ShiftSync-Manager', first_name: 'ShiftSync-Manager',
last_name: 'Test-User', last_name: 'Test-User',
email: 'testuserM@shift-sync.com', email: 'testuserM@shift-sync.com',
accessLevel: 100,
is_ss_admin: false is_ss_admin: false
}, },
{ {
@ -45,6 +47,7 @@ const users = [
first_name: 'ShiftSync-Scheduler', first_name: 'ShiftSync-Scheduler',
last_name: 'Test-User', last_name: 'Test-User',
email: 'testuserS@shift-sync.com', email: 'testuserS@shift-sync.com',
accessLevel: 50,
is_ss_admin: false is_ss_admin: false
}, },
{ {
@ -52,65 +55,66 @@ const users = [
first_name: 'ShiftSync-User', first_name: 'ShiftSync-User',
last_name: 'Test-User', last_name: 'Test-User',
email: 'testuserU@shift-sync.com', email: 'testuserU@shift-sync.com',
accessLevel: 1,
is_ss_admin: false is_ss_admin: false
}, },
] ];
const AppRouter = () => { const AppRouter = () => {
const { user, setUser, setDepartment } = useLocalStore(); const { user, setUser, setDepartment } = useLocalStore();
const [userChanged, setUserChanged] = useState(false); const [userChanged, setUserChanged] = useState(false);
const isDev = true; // change for it.
const fetchAPI = async () => {
const location = window.location;
const uri = `${location?.protocol}//${location.hostname}/api`;
const response = await axios.get(uri);
console.log(response.data.fruits);
};
useEffect(() => { useEffect(() => {
const init = async () => {
const localVersion = localStorage.getItem("APP_VERSION"); const localVersion = localStorage.getItem("APP_VERSION");
const currentVersion = window.APP_VERSION; const currentVersion = window.APP_VERSION;
if (localVersion && localVersion !== currentVersion) { if (localVersion && localVersion !== currentVersion) {
console.log("Version changed, forcing reload"); console.log("Version changed, forcing reload");
localStorage.setItem("APP_VERSION", currentVersion); localStorage.setItem("APP_VERSION", currentVersion);
window.location.reload(true); // force full page reload window.location.reload(true);
return;
} else { } else {
localStorage.setItem("APP_VERSION", currentVersion); localStorage.setItem("APP_VERSION", currentVersion);
} }
fetchAPI(); const data = await fetchAPI('userData', 'get');
// await call for getting the count of employees and any other calls to db. console.log('data:', data);
// TODO: Replace this with real data from your API
// const users = data?.users || []; // Example fix
// const dept = data?.dept || {}; // Example fix
const employee_count = 1; const employee_count = 1;
const subs_expiration = '10/22/2025'; const subs_expiration = '10/22/2025';
setUser({ setUser({
...users[0], ...users[0],
scheduler: dept?.schedulers?.includes(1), scheduler: dept?.schedulers?.includes(1),
manager: dept?.managers?.includes(1), manager: dept?.managers?.includes(1),
administrator: dept?.administrators?.includes(1) administrator: dept?.administrators?.includes(1)
}); });
const newAdministrators = dept?.administrators?.map((admin) => { const newAdministrators = dept?.administrators?.map((admin) => {
const user = users?.find((user) => { const user = users?.find((user) => user?.id === admin);
return user?.id === admin;
});
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
}); });
const newManagers = dept?.managers?.map((manager) => { const newManagers = dept?.managers?.map((manager) => {
const user = users?.find((user) => { const user = users?.find((user) => user?.id === manager);
return user?.id === manager;
});
return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
}); });
const newSchedulers = dept?.schedulers?.map((scheduler) => { const newSchedulers = dept?.schedulers?.map((scheduler) => {
const user = users?.find((user) => { const user = users?.find((user) => user?.id === scheduler);
return 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}` }; return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` };
}); });
const newUsers = users?.map((user) => ({
id: user?.id,
value: `${user?.last_name}, ${user?.first_name}`
}));
setDepartment({ setDepartment({
...dept, ...dept,
users: newUsers, users: newUsers,
@ -120,6 +124,9 @@ const AppRouter = () => {
employee_count, employee_count,
subs_expiration subs_expiration
}); });
};
init();
}, []); }, []);
useEffect(() => { useEffect(() => {

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);
}
};