From a0bb4bef4a236b9e194255e7f8b9a611031d73a1 Mon Sep 17 00:00:00 2001 From: Matt DiMeglio Date: Mon, 23 Jun 2025 17:55:42 -0400 Subject: [PATCH] Init API Side --- .gitignore | 2 + api/package-lock.json | 149 +++++++++++++++++- api/package.json | 3 +- api/router/rest/index.js | 6 + api/router/rest/userData/index.js | 17 ++ api/router/routes.js | 6 + api/server.js | 8 +- api/services/connection.js | 27 ++++ api/services/index.js | 7 + api/services/operations/userData.js | 15 ++ api/services/postgres/postgresServices.js | 8 +- .../common/TransferBox/TransferBox.jsx | 3 +- web/components/stores/useLocalStore.js | 2 + web/package-lock.json | 8 +- web/package.json | 2 +- web/src/pages/Settings/Settings.jsx | 9 +- web/src/router/AppRouter.jsx | 59 ++++--- web/src/router/axios.js | 27 ++++ 18 files changed, 319 insertions(+), 39 deletions(-) create mode 100644 api/router/rest/index.js create mode 100644 api/router/rest/userData/index.js create mode 100644 api/router/routes.js create mode 100644 api/services/connection.js create mode 100644 api/services/index.js create mode 100644 api/services/operations/userData.js create mode 100644 web/src/router/axios.js diff --git a/.gitignore b/.gitignore index a547bf3..d7de12f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ dist dist-ssr *.local +.env + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/api/package-lock.json b/api/package-lock.json index 35bdfc5..b111f52 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "cors": "^2.8.5", "dotenv": "^16.5.0", - "express": "^5.1.0" + "express": "^5.1.0", + "pg": "^8.16.2" }, "devDependencies": { "nodemon": "^3.1.10" @@ -831,6 +832,95 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -844,6 +934,45 @@ "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1099,6 +1228,15 @@ "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": { "version": "2.0.1", "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", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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" + } } } } diff --git a/api/package.json b/api/package.json index 6845610..4ead34c 100644 --- a/api/package.json +++ b/api/package.json @@ -11,7 +11,8 @@ "dependencies": { "cors": "^2.8.5", "dotenv": "^16.5.0", - "express": "^5.1.0" + "express": "^5.1.0", + "pg": "^8.16.2" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/api/router/rest/index.js b/api/router/rest/index.js new file mode 100644 index 0000000..82abf58 --- /dev/null +++ b/api/router/rest/index.js @@ -0,0 +1,6 @@ +import express from 'express'; +import { userDataRouter } from './userData/index.js'; + +export const restRouter = express.Router(); + +restRouter.use('/userData', userDataRouter); \ No newline at end of file diff --git a/api/router/rest/userData/index.js b/api/router/rest/userData/index.js new file mode 100644 index 0000000..fb9c65a --- /dev/null +++ b/api/router/rest/userData/index.js @@ -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); + } +}) \ No newline at end of file diff --git a/api/router/routes.js b/api/router/routes.js new file mode 100644 index 0000000..a8a33a2 --- /dev/null +++ b/api/router/routes.js @@ -0,0 +1,6 @@ +import express from 'express'; +import { restRouter } from './rest/index.js'; + +export const routes = express.Router(); + +routes.use('/rest', restRouter); \ No newline at end of file diff --git a/api/server.js b/api/server.js index fda9b50..7c28cdd 100644 --- a/api/server.js +++ b/api/server.js @@ -2,7 +2,7 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config(); -import { postgresServices } from './services/postgres/postgresServices.js'; +import { routes } from './router/routes.js'; const app = express(); @@ -15,9 +15,11 @@ const corsOptions = { }; app.use(cors(corsOptions)); +app.use(express.json({ limit: '10mb' })); +app.use('/api', routes); -app.get("/api", (req, res) => { - res.json({ fruits: ["apple", "orange", "banana"] }); +app.get('*route', (req, res) => { + res.send("Hello from ShiftSync"); }); app.listen(5172, () => { diff --git a/api/services/connection.js b/api/services/connection.js new file mode 100644 index 0000000..15b31e4 --- /dev/null +++ b/api/services/connection.js @@ -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(); + } +} diff --git a/api/services/index.js b/api/services/index.js new file mode 100644 index 0000000..1853820 --- /dev/null +++ b/api/services/index.js @@ -0,0 +1,7 @@ +import { + userDataOperations +} from './operations/userData.js'; + +export const databaseServices = { + ...userDataOperations +} \ No newline at end of file diff --git a/api/services/operations/userData.js b/api/services/operations/userData.js new file mode 100644 index 0000000..c404c65 --- /dev/null +++ b/api/services/operations/userData.js @@ -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 +} \ No newline at end of file diff --git a/api/services/postgres/postgresServices.js b/api/services/postgres/postgresServices.js index d6fa75d..31451c2 100644 --- a/api/services/postgres/postgresServices.js +++ b/api/services/postgres/postgresServices.js @@ -1,6 +1,12 @@ export const postgresServices = { 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) => { diff --git a/web/components/common/TransferBox/TransferBox.jsx b/web/components/common/TransferBox/TransferBox.jsx index 0ca1d24..d245b97 100644 --- a/web/components/common/TransferBox/TransferBox.jsx +++ b/web/components/common/TransferBox/TransferBox.jsx @@ -194,7 +194,8 @@ export const TransferBox = ({ fields, leftGroup, rightGroup, onSave, user={} }) { - return onSave(rightItems?.map((item) => { return item?.id })) + onSave(rightItems?.map((item) => { return item?.id })); + setItemSelected({}); }} color='#00B33C' buttonEnabled={hasChanges} diff --git a/web/components/stores/useLocalStore.js b/web/components/stores/useLocalStore.js index 994895c..a4ea550 100644 --- a/web/components/stores/useLocalStore.js +++ b/web/components/stores/useLocalStore.js @@ -5,4 +5,6 @@ export const useLocalStore = create((set) => ({ setUser: (user) => set({ user }), department: null, setDepartment: (department) => set({ department }), + access: null, + setAccess: (access) => set({ access }), })); diff --git a/web/package-lock.json b/web/package-lock.json index 9512d68..38f4da5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", - "axios": "^1.9.0", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.0", @@ -1905,9 +1905,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/web/package.json b/web/package.json index 495e058..bba0a49 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", - "axios": "^1.9.0", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.0", diff --git a/web/src/pages/Settings/Settings.jsx b/web/src/pages/Settings/Settings.jsx index 95c23f9..487368a 100644 --- a/web/src/pages/Settings/Settings.jsx +++ b/web/src/pages/Settings/Settings.jsx @@ -228,6 +228,7 @@ export const Settings = () => { const hasChanges = Object.keys(changedFields).length > 0; const onSubmit = (data) => { + console.log('data: ', data); setDepartment({ ...department, ...data @@ -304,6 +305,10 @@ export const Settings = () => { } }, [tabValue, isAdministrator, isManager, isScheduler]); + useEffect(() => { + + }, [department]); + return (
{user?.administrator || user?.manager ? ( @@ -453,7 +458,9 @@ export const Settings = () => { fields={field} leftGroup={department[field?.origList]} rightGroup={department[field?.id]} - onSave={(t) => console.log(t)} + onSave={(t) => { + return onSubmit({ [field.id]: t }); + }} /> } return field?.type !== 'header' && ( diff --git a/web/src/router/AppRouter.jsx b/web/src/router/AppRouter.jsx index d5dfcb5..30939a2 100644 --- a/web/src/router/AppRouter.jsx +++ b/web/src/router/AppRouter.jsx @@ -3,7 +3,7 @@ import { Routes, Route } from 'react-router-dom'; import { Home, Profile, Schedule, Settings } from '@src/pages'; import { Shell } from '@components'; import { useLocalStore } from '@components'; -import axios from "axios"; +import { fetchAPI } from './axios.js'; const dept = { id: 1, @@ -31,6 +31,7 @@ const users = [ first_name: 'ShiftSync-Administrator', last_name: 'Test-User', email: 'testuserA@shift-sync.com', + accessLevel: 150, is_ss_admin: false }, { @@ -38,6 +39,7 @@ const users = [ first_name: 'ShiftSync-Manager', last_name: 'Test-User', email: 'testuserM@shift-sync.com', + accessLevel: 100, is_ss_admin: false }, { @@ -45,6 +47,7 @@ const users = [ first_name: 'ShiftSync-Scheduler', last_name: 'Test-User', email: 'testuserS@shift-sync.com', + accessLevel: 50, is_ss_admin: false }, { @@ -52,65 +55,66 @@ const users = [ first_name: 'ShiftSync-User', last_name: 'Test-User', email: 'testuserU@shift-sync.com', + accessLevel: 1, is_ss_admin: false }, -] +]; const AppRouter = () => { const { user, setUser, setDepartment } = useLocalStore(); 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(() => { + const init = async () => { const localVersion = localStorage.getItem("APP_VERSION"); const currentVersion = window.APP_VERSION; if (localVersion && localVersion !== currentVersion) { console.log("Version changed, forcing reload"); localStorage.setItem("APP_VERSION", currentVersion); - window.location.reload(true); // force full page reload + window.location.reload(true); + return; } else { localStorage.setItem("APP_VERSION", currentVersion); } - - fetchAPI(); - // await call for getting the count of employees and any other calls to db. + + const data = await fetchAPI('userData', 'get'); + 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 subs_expiration = '10/22/2025'; + setUser({ ...users[0], scheduler: dept?.schedulers?.includes(1), manager: dept?.managers?.includes(1), administrator: dept?.administrators?.includes(1) }); + const newAdministrators = dept?.administrators?.map((admin) => { - const user = users?.find((user) => { - return user?.id === admin; - }); + const user = users?.find((user) => user?.id === admin); return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; }); + const newManagers = dept?.managers?.map((manager) => { - const user = users?.find((user) => { - return user?.id === manager; - }); + const user = users?.find((user) => user?.id === manager); return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; }); + const newSchedulers = dept?.schedulers?.map((scheduler) => { - const user = users?.find((user) => { - return user?.id === scheduler; - }); - return { id: user?.id, value: `${user?.last_name}, ${user?.first_name}` }; - }); - const newUsers = users?.map((user) => { + const user = users?.find((user) => user?.id === scheduler); 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({ ...dept, users: newUsers, @@ -120,6 +124,9 @@ const AppRouter = () => { employee_count, subs_expiration }); + }; + + init(); }, []); useEffect(() => { diff --git a/web/src/router/axios.js b/web/src/router/axios.js new file mode 100644 index 0000000..9f02db1 --- /dev/null +++ b/web/src/router/axios.js @@ -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); + } +};