From 668d0075912dfd49ea26247f499f71a338eb868c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:34:29 -0500 Subject: [PATCH 01/15] user entitie --- dataconnect/connector/user/mutations.gql | 41 ++++++++++++++++++++++ dataconnect/connector/user/queries.gql | 43 ++++++++++++++++++++++++ dataconnect/schema/user.gql | 15 +++++++++ 3 files changed, 99 insertions(+) create mode 100644 dataconnect/connector/user/mutations.gql create mode 100644 dataconnect/connector/user/queries.gql create mode 100644 dataconnect/schema/user.gql diff --git a/dataconnect/connector/user/mutations.gql b/dataconnect/connector/user/mutations.gql new file mode 100644 index 00000000..cf6444fe --- /dev/null +++ b/dataconnect/connector/user/mutations.gql @@ -0,0 +1,41 @@ +mutation CreateUser( + $id: UUID!, # Firebase UID + $email: String!, + $fullName: String!, + $role: UserBaseRole!, + $userRole: String +) @auth(level: USER) { + user_insert( + data: { + id: $id + email: $email + fullName: $fullName + role: $role + userRole: $userRole + } + ) +} + +mutation UpdateUser( + $id: String!, + $email: String, + $fullName: String, + $role: UserBaseRole, + $userRole: String +) @auth(level: USER) { + user_update( + id: $id, + data: { + email: $email + fullName: $fullName + role: $role + userRole: $userRole + } + ) +} + +mutation DeleteUser( + $id: String! +) @auth(level: USER) { + user_delete(id: $id) +} diff --git a/dataconnect/connector/user/queries.gql b/dataconnect/connector/user/queries.gql new file mode 100644 index 00000000..417860bf --- /dev/null +++ b/dataconnect/connector/user/queries.gql @@ -0,0 +1,43 @@ +query listUsers @auth(level: USER) { + users { + id + email + fullName + role + userRole + createdDate + updatedDate + } +} + +query getUserById( + $id: String! +) @auth(level: USER) { + user(id: $id) { + id + email + fullName + role + userRole + } +} + +query filterUsers( + $email: String, + $role: UserBaseRole, + $userRole: String +) @auth(level: USER) { + users( + where: { + email: { eq: $email } + role: { eq: $role } + userRole: { eq: $userRole } + } + ) { + id + email + fullName + role + userRole + } +} diff --git a/dataconnect/schema/user.gql b/dataconnect/schema/user.gql new file mode 100644 index 00000000..b5982534 --- /dev/null +++ b/dataconnect/schema/user.gql @@ -0,0 +1,15 @@ +enum UserBaseRole { + ADMIN + USER +} + +type User @table(name: "users") { + id: UUID! # user_id / uid de Firebase + email: String! + fullName: String! + role: UserBaseRole! + userRole: String + createdDate: Timestamp @default(expr: "request.time") + updatedDate: Timestamp @default(expr: "request.time") + createdBy: String @default(expr: "auth.uid") +} From ab6816c7abc0a531b00eb3e8a5a68b9ba5286ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:42:02 -0500 Subject: [PATCH 02/15] adding/editing 2 entities --- internal-api-harness/src/api/krowSDK.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal-api-harness/src/api/krowSDK.js b/internal-api-harness/src/api/krowSDK.js index 4cfca950..9bada66c 100644 --- a/internal-api-harness/src/api/krowSDK.js +++ b/internal-api-harness/src/api/krowSDK.js @@ -139,19 +139,30 @@ entityNames.forEach(entityName => { });*/ const dataconnectEntityConfig = { + User: { + list: 'listUsers', + get: 'getUserById', + create: 'createUser', + update: 'updateUser', + delete: 'deleteUser', + filter: 'filterUsers', + }, Event: { list: 'listEvents', create: 'createEvent', - // get: 'getEvent', - // update: 'updateEvent', - // delete: 'deleteEvent', - // filter: 'filterEvents', + get: 'getEventById', + update: 'updateEvent', + delete: 'deleteEvent', + filter: 'filterEvents', }, Staff: { list: 'listStaff', create: 'createStaff', - + get: 'getStaffById', + update: 'updateStaff', + delete: 'deleteStaff', + filter: 'filterStaff', }, Vendor: { From 4dc1553174883b45463612b4adc971dbc47f1e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:46:31 -0500 Subject: [PATCH 03/15] modified user id type --- dataconnect/connector/user/mutations.gql | 4 ++-- dataconnect/connector/user/queries.gql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dataconnect/connector/user/mutations.gql b/dataconnect/connector/user/mutations.gql index cf6444fe..f0da8002 100644 --- a/dataconnect/connector/user/mutations.gql +++ b/dataconnect/connector/user/mutations.gql @@ -17,7 +17,7 @@ mutation CreateUser( } mutation UpdateUser( - $id: String!, + $id: UUID!, $email: String, $fullName: String, $role: UserBaseRole, @@ -35,7 +35,7 @@ mutation UpdateUser( } mutation DeleteUser( - $id: String! + $id: UUID! ) @auth(level: USER) { user_delete(id: $id) } diff --git a/dataconnect/connector/user/queries.gql b/dataconnect/connector/user/queries.gql index 417860bf..6675c691 100644 --- a/dataconnect/connector/user/queries.gql +++ b/dataconnect/connector/user/queries.gql @@ -11,7 +11,7 @@ query listUsers @auth(level: USER) { } query getUserById( - $id: String! + $id: UUID! ) @auth(level: USER) { user(id: $id) { id From 4cbb3c429a478098186cfe90819adb8ac27bd122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:33:03 -0500 Subject: [PATCH 04/15] new firebase file for frontend-web --- frontend-web/src/firebase.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 frontend-web/src/firebase.js diff --git a/frontend-web/src/firebase.js b/frontend-web/src/firebase.js new file mode 100644 index 00000000..9f5e6fb4 --- /dev/null +++ b/frontend-web/src/firebase.js @@ -0,0 +1,20 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getDataConnect } from 'firebase/data-connect'; +import { connectorConfig } from '@dataconnect/generated'; + +// Your web app's Firebase configuration +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +export const dataConnect = getDataConnect(app, connectorConfig); +export const auth = getAuth(app); \ No newline at end of file From 7a1da8c0d477b9f6cd736675334126484b10b7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:59:29 -0500 Subject: [PATCH 05/15] auth working in webpage --- .../src/components/auth/ProtectedRoute.jsx | 25 + .../src/components/auth/PublicRoute.jsx | 25 + frontend-web/src/hooks/useAuth.js | 19 + frontend-web/src/pages/Login.jsx | 86 +++ frontend-web/src/pages/Register.jsx | 128 ++++ frontend-web/src/pages/index.jsx | 706 ++++++------------ 6 files changed, 531 insertions(+), 458 deletions(-) create mode 100644 frontend-web/src/components/auth/ProtectedRoute.jsx create mode 100644 frontend-web/src/components/auth/PublicRoute.jsx create mode 100644 frontend-web/src/hooks/useAuth.js create mode 100644 frontend-web/src/pages/Login.jsx create mode 100644 frontend-web/src/pages/Register.jsx diff --git a/frontend-web/src/components/auth/ProtectedRoute.jsx b/frontend-web/src/components/auth/ProtectedRoute.jsx new file mode 100644 index 00000000..22384a45 --- /dev/null +++ b/frontend-web/src/components/auth/ProtectedRoute.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '@/hooks/useAuth'; +import { Loader2 } from 'lucide-react'; + +export default function ProtectedRoute({ children }) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + if (!user) { + return ; + } + + return children; +} \ No newline at end of file diff --git a/frontend-web/src/components/auth/PublicRoute.jsx b/frontend-web/src/components/auth/PublicRoute.jsx new file mode 100644 index 00000000..55d4d0d8 --- /dev/null +++ b/frontend-web/src/components/auth/PublicRoute.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '@/hooks/useAuth'; +import { Loader2 } from 'lucide-react'; + +export default function PublicRoute({ children }) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + if (user) { + return ; + } + + return children; +} \ No newline at end of file diff --git a/frontend-web/src/hooks/useAuth.js b/frontend-web/src/hooks/useAuth.js new file mode 100644 index 00000000..51d3b33e --- /dev/null +++ b/frontend-web/src/hooks/useAuth.js @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; +import { onAuthStateChanged } from 'firebase/auth'; +import { auth } from '@/firebase'; + +export function useAuth() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setUser(user); + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + return { user, loading }; +} \ No newline at end of file diff --git a/frontend-web/src/pages/Login.jsx b/frontend-web/src/pages/Login.jsx new file mode 100644 index 00000000..c5263c9f --- /dev/null +++ b/frontend-web/src/pages/Login.jsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { signInWithEmailAndPassword } from "firebase/auth"; +import { auth } from "@/firebase"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Loader2 } from "lucide-react"; + +export default function Login() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleLogin = async (e) => { + e.preventDefault(); + setError(null); + + if (!email || !password) { + setError("Email and password are required."); + return; + } + + setLoading(true); + try { + await signInWithEmailAndPassword(auth, email, password); + navigate("/"); + } catch (error) { + setError("Invalid credentials. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + Login + Enter your credentials to access your account. + + +
+
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ {error &&

{error}

} +
+
+
+ + +

+ Don't have an account?{" "} + + Register + +

+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend-web/src/pages/Register.jsx b/frontend-web/src/pages/Register.jsx new file mode 100644 index 00000000..7e1931e5 --- /dev/null +++ b/frontend-web/src/pages/Register.jsx @@ -0,0 +1,128 @@ +import React, { useState } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { createUserWithEmailAndPassword } from "firebase/auth"; +import { auth } from "@/firebase"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Loader2 } from "lucide-react"; + +export default function Register() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const validatePassword = (password) => { + if (password.length < 6) { + return "Password must be at least 6 characters long."; + } + return null; + }; + + const validateEmail = (email) => { + const re = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + if (!re.test(String(email).toLowerCase())) { + return "Invalid email address."; + } + return null; + }; + + const handleRegister = async (e) => { + e.preventDefault(); + setError(null); + + if (!email || !password) { + setError("Email and password are required."); + return; + } + + const emailError = validateEmail(email); + if (emailError) { + setError(emailError); + return; + } + + const passwordError = validatePassword(password); + if (passwordError) { + setError(passwordError); + return; + } + + setLoading(true); + try { + await createUserWithEmailAndPassword(auth, email, password); + navigate("/"); + } catch (error) { + setError(error.message || "Something went wrong. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + Register + Create a new account. + + + +
+
+
+ + setEmail(e.target.value)} + autoComplete="email" + /> +
+ +
+ + setPassword(e.target.value)} + autoComplete="new-password" + /> +
+ + {error &&

{error}

} +
+
+
+ + + + +

+ Already have an account?{" "} + + Login + +

+
+
+
+ ); +} diff --git a/frontend-web/src/pages/index.jsx b/frontend-web/src/pages/index.jsx index c3d5213d..6caeef9f 100644 --- a/frontend-web/src/pages/index.jsx +++ b/frontend-web/src/pages/index.jsx @@ -1,477 +1,267 @@ -import Layout from "./Layout.jsx"; - -import Dashboard from "./Dashboard"; - -import StaffDirectory from "./StaffDirectory"; - -import AddStaff from "./AddStaff"; - -import EditStaff from "./EditStaff"; - -import Events from "./Events"; - -import CreateEvent from "./CreateEvent"; - -import EditEvent from "./EditEvent"; - -import EventDetail from "./EventDetail"; - -import Business from "./Business"; - -import Invoices from "./Invoices"; - -import Payroll from "./Payroll"; - -import Certification from "./Certification"; - -import Support from "./Support"; - -import Reports from "./Reports"; - -import Settings from "./Settings"; - -import ActivityLog from "./ActivityLog"; - -import AddBusiness from "./AddBusiness"; - -import EditBusiness from "./EditBusiness"; - -import ProcurementDashboard from "./ProcurementDashboard"; - -import OperatorDashboard from "./OperatorDashboard"; - -import VendorDashboard from "./VendorDashboard"; - -import WorkforceDashboard from "./WorkforceDashboard"; - -import Messages from "./Messages"; - -import ClientDashboard from "./ClientDashboard"; - -import Onboarding from "./Onboarding"; - -import ClientOrders from "./ClientOrders"; - -import ClientInvoices from "./ClientInvoices"; - -import VendorOrders from "./VendorOrders"; - -import VendorStaff from "./VendorStaff"; - -import VendorInvoices from "./VendorInvoices"; - -import VendorPerformance from "./VendorPerformance"; - -import WorkforceShifts from "./WorkforceShifts"; - -import WorkforceEarnings from "./WorkforceEarnings"; - -import WorkforceProfile from "./WorkforceProfile"; - -import UserManagement from "./UserManagement"; - -import Home from "./Home"; - -import VendorRateCard from "./VendorRateCard"; - -import Permissions from "./Permissions"; - -import WorkforceCompliance from "./WorkforceCompliance"; - -import Teams from "./Teams"; - -import CreateTeam from "./CreateTeam"; - -import TeamDetails from "./TeamDetails"; - -import VendorManagement from "./VendorManagement"; - -import PartnerManagement from "./PartnerManagement"; - -import EnterpriseManagement from "./EnterpriseManagement"; - -import VendorOnboarding from "./VendorOnboarding"; - -import SectorManagement from "./SectorManagement"; - -import AddEnterprise from "./AddEnterprise"; - -import AddSector from "./AddSector"; - -import AddPartner from "./AddPartner"; - -import EditVendor from "./EditVendor"; - -import SmartVendorOnboarding from "./SmartVendorOnboarding"; - -import InviteVendor from "./InviteVendor"; - -import VendorCompliance from "./VendorCompliance"; - -import EditPartner from "./EditPartner"; - -import EditSector from "./EditSector"; - -import EditEnterprise from "./EditEnterprise"; - -import VendorRates from "./VendorRates"; - -import VendorDocumentReview from "./VendorDocumentReview"; - -import VendorMarketplace from "./VendorMarketplace"; - -import RapidOrder from "./RapidOrder"; - -import SmartScheduler from "./SmartScheduler"; - -import StaffOnboarding from "./StaffOnboarding"; - -import NotificationSettings from "./NotificationSettings"; - -import TaskBoard from "./TaskBoard"; - -import InvoiceDetail from "./InvoiceDetail"; - -import InvoiceEditor from "./InvoiceEditor"; - -//import api-docs-raw from "./api-docs-raw"; - -import Tutorials from "./Tutorials"; - -import Schedule from "./Schedule"; - -import StaffAvailability from "./StaffAvailability"; - -import WorkerShiftProposals from "./WorkerShiftProposals"; - import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom'; +// Auth components +import ProtectedRoute from '@/components/auth/ProtectedRoute'; +import PublicRoute from '@/components/auth/PublicRoute'; + +// Layout and pages +import Layout from "./Layout.jsx"; +import Home from "./Home"; +import Login from "./Login"; +import Register from "./Register"; +import Dashboard from "./Dashboard"; +import StaffDirectory from "./StaffDirectory"; +import AddStaff from "./AddStaff"; +import EditStaff from "./EditStaff"; +import Events from "./Events"; +import CreateEvent from "./CreateEvent"; +import EditEvent from "./EditEvent"; +import EventDetail from "./EventDetail"; +import Business from "./Business"; +import Invoices from "./Invoices"; +import Payroll from "./Payroll"; +import Certification from "./Certification"; +import Support from "./Support"; +import Reports from "./Reports"; +import Settings from "./Settings"; +import ActivityLog from "./ActivityLog"; +import AddBusiness from "./AddBusiness"; +import EditBusiness from "./EditBusiness"; +import ProcurementDashboard from "./ProcurementDashboard"; +import OperatorDashboard from "./OperatorDashboard"; +import VendorDashboard from "./VendorDashboard"; +import WorkforceDashboard from "./WorkforceDashboard"; +import Messages from "./Messages"; +import ClientDashboard from "./ClientDashboard"; +import Onboarding from "./Onboarding"; +import ClientOrders from "./ClientOrders"; +import ClientInvoices from "./ClientInvoices"; +import VendorOrders from "./VendorOrders"; +import VendorStaff from "./VendorStaff"; +import VendorInvoices from "./VendorInvoices"; +import VendorPerformance from "./VendorPerformance"; +import WorkforceShifts from "./WorkforceShifts"; +import WorkforceEarnings from "./WorkforceEarnings"; +import WorkforceProfile from "./WorkforceProfile"; +import UserManagement from "./UserManagement"; +import VendorRateCard from "./VendorRateCard"; +import Permissions from "./Permissions"; +import WorkforceCompliance from "./WorkforceCompliance"; +import Teams from "./Teams"; +import CreateTeam from "./CreateTeam"; +import TeamDetails from "./TeamDetails"; +import VendorManagement from "./VendorManagement"; +import PartnerManagement from "./PartnerManagement"; +import EnterpriseManagement from "./EnterpriseManagement"; +import VendorOnboarding from "./VendorOnboarding"; +import SectorManagement from "./SectorManagement"; +import AddEnterprise from "./AddEnterprise"; +import AddSector from "./AddSector"; +import AddPartner from "./AddPartner"; +import EditVendor from "./EditVendor"; +import SmartVendorOnboarding from "./SmartVendorOnboarding"; +import InviteVendor from "./InviteVendor"; +import VendorCompliance from "./VendorCompliance"; +import EditPartner from "./EditPartner"; +import EditSector from "./EditSector"; +import EditEnterprise from "./EditEnterprise"; +import VendorRates from "./VendorRates"; +import VendorDocumentReview from "./VendorDocumentReview"; +import VendorMarketplace from "./VendorMarketplace"; +import RapidOrder from "./RapidOrder"; +import SmartScheduler from "./SmartScheduler"; +import StaffOnboarding from "./StaffOnboarding"; +import NotificationSettings from "./NotificationSettings"; +import TaskBoard from "./TaskBoard"; +import InvoiceDetail from "./InvoiceDetail"; +import InvoiceEditor from "./InvoiceEditor"; +import Tutorials from "./Tutorials"; +import Schedule from "./Schedule"; +import StaffAvailability from "./StaffAvailability"; +import WorkerShiftProposals from "./WorkerShiftProposals"; + const PAGES = { - - Dashboard: Dashboard, - - StaffDirectory: StaffDirectory, - - AddStaff: AddStaff, - - EditStaff: EditStaff, - - Events: Events, - - CreateEvent: CreateEvent, - - EditEvent: EditEvent, - - EventDetail: EventDetail, - - Business: Business, - - Invoices: Invoices, - - Payroll: Payroll, - - Certification: Certification, - - Support: Support, - - Reports: Reports, - - Settings: Settings, - - ActivityLog: ActivityLog, - - AddBusiness: AddBusiness, - - EditBusiness: EditBusiness, - - ProcurementDashboard: ProcurementDashboard, - - OperatorDashboard: OperatorDashboard, - - VendorDashboard: VendorDashboard, - - WorkforceDashboard: WorkforceDashboard, - - Messages: Messages, - - ClientDashboard: ClientDashboard, - - Onboarding: Onboarding, - - ClientOrders: ClientOrders, - - ClientInvoices: ClientInvoices, - - VendorOrders: VendorOrders, - - VendorStaff: VendorStaff, - - VendorInvoices: VendorInvoices, - - VendorPerformance: VendorPerformance, - - WorkforceShifts: WorkforceShifts, - - WorkforceEarnings: WorkforceEarnings, - - WorkforceProfile: WorkforceProfile, - - UserManagement: UserManagement, - - Home: Home, - - VendorRateCard: VendorRateCard, - - Permissions: Permissions, - - WorkforceCompliance: WorkforceCompliance, - - Teams: Teams, - - CreateTeam: CreateTeam, - - TeamDetails: TeamDetails, - - VendorManagement: VendorManagement, - - PartnerManagement: PartnerManagement, - - EnterpriseManagement: EnterpriseManagement, - - VendorOnboarding: VendorOnboarding, - - SectorManagement: SectorManagement, - - AddEnterprise: AddEnterprise, - - AddSector: AddSector, - - AddPartner: AddPartner, - - EditVendor: EditVendor, - - SmartVendorOnboarding: SmartVendorOnboarding, - - InviteVendor: InviteVendor, - - VendorCompliance: VendorCompliance, - - EditPartner: EditPartner, - - EditSector: EditSector, - - EditEnterprise: EditEnterprise, - - VendorRates: VendorRates, - - VendorDocumentReview: VendorDocumentReview, - - VendorMarketplace: VendorMarketplace, - - RapidOrder: RapidOrder, - - SmartScheduler: SmartScheduler, - - StaffOnboarding: StaffOnboarding, - - NotificationSettings: NotificationSettings, - - TaskBoard: TaskBoard, - - InvoiceDetail: InvoiceDetail, - - InvoiceEditor: InvoiceEditor, - - //api-docs-raw: api-docs-raw, - - Tutorials: Tutorials, - - Schedule: Schedule, - - StaffAvailability: StaffAvailability, - - WorkerShiftProposals: WorkerShiftProposals, - -} + Dashboard, + StaffDirectory, + AddStaff, + EditStaff, + Events, + CreateEvent, + EditEvent, + EventDetail, + Business, + Invoices, + Payroll, + Certification, + Support, + Reports, + Settings, + ActivityLog, + AddBusiness, + EditBusiness, + ProcurementDashboard, + OperatorDashboard, + VendorDashboard, + WorkforceDashboard, + Messages, + ClientDashboard, + Onboarding, + ClientOrders, + ClientInvoices, + VendorOrders, + VendorStaff, + VendorInvoices, + VendorPerformance, + WorkforceShifts, + WorkforceEarnings, + WorkforceProfile, + UserManagement, + Home, + VendorRateCard, + Permissions, + WorkforceCompliance, + Teams, + CreateTeam, + TeamDetails, + VendorManagement, + PartnerManagement, + EnterpriseManagement, + VendorOnboarding, + SectorManagement, + AddEnterprise, + AddSector, + AddPartner, + EditVendor, + SmartVendorOnboarding, + InviteVendor, + VendorCompliance, + EditPartner, + EditSector, + EditEnterprise, + VendorRates, + VendorDocumentReview, + VendorMarketplace, + RapidOrder, + SmartScheduler, + StaffOnboarding, + NotificationSettings, + TaskBoard, + InvoiceDetail, + InvoiceEditor, + Tutorials, + Schedule, + StaffAvailability, + WorkerShiftProposals, +}; function _getCurrentPage(url) { - if (url.endsWith('/')) { - url = url.slice(0, -1); - } - let urlLastPart = url.split('/').pop(); - if (urlLastPart.includes('?')) { - urlLastPart = urlLastPart.split('?')[0]; - } - - const pageName = Object.keys(PAGES).find(page => page.toLowerCase() === urlLastPart.toLowerCase()); - return pageName || Object.keys(PAGES)[0]; + if (url.endsWith('/')) url = url.slice(0, -1); + let last = url.split('/').pop(); + if (last.includes('?')) last = last.split('?')[0]; + const pageName = Object.keys(PAGES).find(p => p.toLowerCase() === last.toLowerCase()); + return pageName || 'Home'; // Default to Home } -// Create a wrapper component that uses useLocation inside the Router context -function PagesContent() { + +function AppRoutes() { const location = useLocation(); const currentPage = _getCurrentPage(location.pathname); - + return ( - - - - } /> - - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - - + + {/* Public Routes */} + } /> + } /> + + {/* Private Routes */} + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> + ); } export default function Pages() { return ( - + ); } \ No newline at end of file From 6c574651735c6ef816ecf4df00d1ef3cb89a1923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:29:11 -0500 Subject: [PATCH 06/15] adding logout --- frontend-web/src/pages/Layout.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend-web/src/pages/Layout.jsx b/frontend-web/src/pages/Layout.jsx index 1c5ca921..a5e5a21f 100644 --- a/frontend-web/src/pages/Layout.jsx +++ b/frontend-web/src/pages/Layout.jsx @@ -4,6 +4,8 @@ import { Link, useLocation, useNavigate } from "react-router-dom"; import { createPageUrl } from "@/utils"; import { base44 } from "@/api/base44Client"; import { useQuery } from "@tanstack/react-query"; +import { auth } from "@/firebase"; +import { signOut } from "firebase/auth"; import { Users, LayoutDashboard, UserPlus, Calendar, Briefcase, FileText, DollarSign, Award, HelpCircle, BarChart3, Activity, Menu, MessageSquare, @@ -279,7 +281,7 @@ export default function Layout({ children }) { const userInitial = userName.charAt(0).toUpperCase(); const handleLogout = () => { - base44.auth.logout(); + signOut(auth); }; const handleRefresh = () => { @@ -482,6 +484,10 @@ export default function Layout({ children }) { My Profile + + + Logout + From aaa7aa98d9ed0845c0587c0221c29c2a5ac159e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:00:49 -0500 Subject: [PATCH 07/15] modification id for user --- dataconnect/connector/user/mutations.gql | 6 +++--- dataconnect/connector/user/queries.gql | 2 +- dataconnect/schema/user.gql | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dataconnect/connector/user/mutations.gql b/dataconnect/connector/user/mutations.gql index f0da8002..eeedba82 100644 --- a/dataconnect/connector/user/mutations.gql +++ b/dataconnect/connector/user/mutations.gql @@ -1,5 +1,5 @@ mutation CreateUser( - $id: UUID!, # Firebase UID + $id: String!, # Firebase UID $email: String!, $fullName: String!, $role: UserBaseRole!, @@ -17,7 +17,7 @@ mutation CreateUser( } mutation UpdateUser( - $id: UUID!, + $id: String!, $email: String, $fullName: String, $role: UserBaseRole, @@ -35,7 +35,7 @@ mutation UpdateUser( } mutation DeleteUser( - $id: UUID! + $id: String! ) @auth(level: USER) { user_delete(id: $id) } diff --git a/dataconnect/connector/user/queries.gql b/dataconnect/connector/user/queries.gql index 6675c691..417860bf 100644 --- a/dataconnect/connector/user/queries.gql +++ b/dataconnect/connector/user/queries.gql @@ -11,7 +11,7 @@ query listUsers @auth(level: USER) { } query getUserById( - $id: UUID! + $id: String! ) @auth(level: USER) { user(id: $id) { id diff --git a/dataconnect/schema/user.gql b/dataconnect/schema/user.gql index b5982534..ec927890 100644 --- a/dataconnect/schema/user.gql +++ b/dataconnect/schema/user.gql @@ -4,7 +4,7 @@ enum UserBaseRole { } type User @table(name: "users") { - id: UUID! # user_id / uid de Firebase + id: String! # user_id / uid de Firebase email: String! fullName: String! role: UserBaseRole! From 4356e7d02cd5a4b47b8af9ab8c60d1f0f1e8f322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:48:28 -0500 Subject: [PATCH 08/15] solving problems for missing data in the from --- dataconnect/connector/event/queries.gql | 2 ++ dataconnect/connector/user/queries.gql | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dataconnect/connector/event/queries.gql b/dataconnect/connector/event/queries.gql index c8c1c98b..3fe4233e 100644 --- a/dataconnect/connector/event/queries.gql +++ b/dataconnect/connector/event/queries.gql @@ -35,6 +35,7 @@ query listEvents @auth(level: USER) { notes requested assignedStaff + createdBy } } @@ -143,5 +144,6 @@ query filterEvents( notes requested assignedStaff + createdBy } } diff --git a/dataconnect/connector/user/queries.gql b/dataconnect/connector/user/queries.gql index 417860bf..d761fdc6 100644 --- a/dataconnect/connector/user/queries.gql +++ b/dataconnect/connector/user/queries.gql @@ -23,12 +23,14 @@ query getUserById( } query filterUsers( + $id: String, $email: String, $role: UserBaseRole, $userRole: String ) @auth(level: USER) { users( where: { + id: { eq: $id } email: { eq: $email } role: { eq: $role } userRole: { eq: $userRole } From fc31fbd4a3a7709e62594ec2aa84253b77efebc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:49:35 -0500 Subject: [PATCH 09/15] important configuration for dataconnect and firebase --- frontend-web/src/api/client.js | 22 ++ frontend-web/src/api/krowSDK.js | 466 ++++++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 frontend-web/src/api/client.js create mode 100644 frontend-web/src/api/krowSDK.js diff --git a/frontend-web/src/api/client.js b/frontend-web/src/api/client.js new file mode 100644 index 00000000..e63a00f7 --- /dev/null +++ b/frontend-web/src/api/client.js @@ -0,0 +1,22 @@ +import axios from "axios"; +import { auth } from "../firebase"; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, // You will need to add this to your .env file +}); + +apiClient.interceptors.request.use( + async (config) => { + const user = auth.currentUser; + if (user) { + const token = await user.getIdToken(); + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +export default apiClient; \ No newline at end of file diff --git a/frontend-web/src/api/krowSDK.js b/frontend-web/src/api/krowSDK.js new file mode 100644 index 00000000..45d6c64f --- /dev/null +++ b/frontend-web/src/api/krowSDK.js @@ -0,0 +1,466 @@ +import apiClient from './client'; +import { auth, dataConnect } from '../firebase'; +import { signOut } from 'firebase/auth'; + +import * as dcSdk from '@dataconnect/generated'; // listEvents, createEvent, etc. + +// --- Auth Module --- +const authModule = { + /** + * Fetches the currently authenticated user's profile from the backend. + * @returns {Promise} The user profile. + */ + me: async () => { + const { data } = await apiClient.get('/auth/me'); + return data; + }, + + /** + * Logs the user out. + * @param {string} [redirectUrl] - Optional URL to redirect to after logout. + */ + logout: async (redirectUrl) => { + await signOut(auth); + if (redirectUrl) { + window.location.href = redirectUrl; + } + }, + + /** + * Checks if a user is currently authenticated. + * @returns {boolean} True if a user is authenticated. + */ + isAuthenticated: () => { + return !!auth.currentUser; + }, +}; + +// --- Core Integrations Module --- +const coreIntegrationsModule = { + /** + * Sends an email. + * @param {object} params - { to, subject, body } + * @returns {Promise} API response. + */ + SendEmail: async (params) => { + const { data } = await apiClient.post('/sendEmail', params); + return data; + }, + + /** + * Invokes a large language model. + * @param {object} params - { prompt, response_json_schema, file_urls } + * @returns {Promise} API response. + */ + InvokeLLM: async (params) => { + const { data } = await apiClient.post('/invokeLLM', params); + return data; + }, + + /** + * Uploads a public file. + * @param {File} file - The file to upload. + * @returns {Promise} API response with file_url. + */ + UploadFile: async ({ file }) => { + const formData = new FormData(); + formData.append('file', file); + const { data } = await apiClient.post('/uploadFile', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return data; + }, + + /** + * Uploads a private file. + * @param {File} file - The file to upload. + * @returns {Promise} API response with file_uri. + */ + UploadPrivateFile: async ({ file }) => { + const formData = new FormData(); + formData.append('file', file); + const { data } = await apiClient.post('/uploadPrivateFile', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return data; + }, + + /** + * Creates a temporary signed URL for a private file. + * @param {object} params - { file_uri, expires_in } + * @returns {Promise} API response with signed_url. + */ + CreateFileSignedUrl: async (params) => { + const { data } = await apiClient.post('/createSignedUrl', params); + return data; + }, +}; + +const dataconnectEntityConfig = { + User: { + list: 'listUsers', + get: 'getUserById', + create: 'createUser', + update: 'updateUser', + delete: 'deleteUser', + filter: 'filterUsers', + }, + Event: { + list: 'listEvents', + create: 'createEvent', + get: 'getEventById', + update: 'updateEvent', + delete: 'deleteEvent', + filter: 'filterEvents', + }, + + Staff: { + list: 'listStaff', + create: 'createStaff', + get: 'getStaffById', + update: 'updateStaff', + delete: 'deleteStaff', + filter: 'filterStaff', + }, + + Vendor: { + list: 'listVendor', + get: 'getVendorById', + create: 'createVendor', + update: 'updateVendor', + delete: 'deleteVendor', + filter: 'filterVendors', + }, + + VendorRate: { + list: 'listVendorRate', + get: 'getVendorRateById', + create: 'createVendorRate', + update: 'updateVendorRate', + delete: 'deleteVendorRate', + filter: 'filterVendorRates', + }, + + VendorDefaultSetting:{ + list: 'listVendorDefaultSettings', + get: 'getVendorDefaultSettingById', + create: 'createVendorDefaultSetting', + update: 'updateVendorDefaultSetting', + delete: 'deleteVendorDefaultSetting', + filter: 'filterVendorDefaultSettings', + }, + + Invoice:{ + list: 'listInvoice', + get: 'getInvoiceById', + create: 'createInvoice', + update: 'updateInvoice', + delete: 'deleteInvoice', + filter: 'filterInvoices', + + }, + + Business:{ + list: 'listBusiness', + get: 'getBusinessById', + create: 'createBusiness', + update: 'updateBusiness', + delete: 'deleteBusiness', + filter: 'filterBusiness', + }, + + Certification:{ + list: 'listCertification', + get: 'getCertificationById', + create: 'createCertification', + update: 'updateCertification', + delete: 'deleteCertification', + filter: 'filterCertification', + }, + + Team:{ + list: 'listTeam', + get: 'getTeamById', + create: 'createTeam', + update: 'updateTeam', + delete: 'deleteTeam', + filter: 'filterTeam', + }, + + TeamMember: { + list: 'listTeamMember', + get: 'getTeamMemberById', + create: 'createTeamMember', + update: 'updateTeamMember', + delete: 'deleteTeamMember', + filter: 'filterTeamMember', + }, + + TeamHub: { + list: 'listTeamHub', + get: 'getTeamHubById', + create: 'createTeamHub', + update: 'updateTeamHub', + delete: 'deleteTeamHub', + filter: 'filterTeamHub', + }, + + TeamMemberInvite: { + list: 'listTeamMemberInvite', + get: 'getTeamMemberInviteById', + create: 'createTeamMemberInvite', + update: 'updateTeamMemberInvite', + delete: 'deleteTeamMemberInvite', + filter: 'filterTeamMemberInvite', + }, + + Conversation:{ + list: 'listConversation', + get: 'getConversationById', + create: 'createConversation', + update: 'updateConversation', + delete: 'deleteConversation', + filter: 'filterConversation', + }, + + Message:{ + list: 'listMessage', + get: 'getMessageById', + create: 'createMessage', + update: 'updateMessage', + delete: 'deleteMessage', + filter: 'filterMessage', + }, + + ActivityLog:{ + list: 'listActivityLog', + get: 'getActivityLogById', + create: 'createActivityLog', + update: 'updateActivityLog', + delete: 'deleteActivityLog', + filter: 'filterActivityLog', + }, + + Enterprise:{ + list: 'listEnterprise', + get: 'getEnterpriseById', + create: 'createEnterprise', + update: 'updateEnterprise', + delete: 'deleteEnterprise', + filter: 'filterEnterprise', + }, + + Sector:{ + + }, + + Partner:{ + + }, + + Order:{ + + }, + + Shift:{ + + } + +}; + +// Helper for methods not implemented +const notImplemented = (entityName, method) => async () => { + throw new Error(`${entityName}.${method} is not implemented yet for Data Connect`); +}; + +// --- Entities Module ( Data Connect, without REST Base44) --- +const entitiesModule = {}; + +Object.entries(dataconnectEntityConfig).forEach(([entityName, ops]) => { + entitiesModule[entityName] = { + + get: notImplemented(entityName, 'get'), + update: notImplemented(entityName, 'update'), + delete: notImplemented(entityName, 'delete'), + filter: notImplemented(entityName, 'filter'), + list: notImplemented(entityName, 'list'), + create: notImplemented(entityName, 'create'), + + // list + ...(ops.list && { + list: async (params) => { + const fn = dcSdk[ops.list]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.list}" not found for entity "${entityName}".` + ); + } + + return fn(dataConnect); + }, + }), + + // create + ...(ops.create && { + create: async (params) => { + const fn = dcSdk[ops.create]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.create}" not found for entity "${entityName}".` + ); + } + + const { data } = params ?? {}; + if (!data) { + throw new Error( + `${entityName}.create expects a payload like { data: { ...fields } }` + ); + } + + return fn(dataConnect, data); + }, + }), + + //get + ...(ops.get && { + get: async (params) => { + const fn = dcSdk[ops.get]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.get}" not found for entity "${entityName}".` + ); + } + + if (!params || typeof params !== 'object') { + throw new Error(`${entityName}.get expects an object of variables (e.g. { id })`); + } + + return fn(dataConnect, params); + }, + }), + + //update + ...(ops.update && { + update: async (params) => { + const fn = dcSdk[ops.update]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.update}" not found for entity "${entityName}".` + ); + } + + if (!params || typeof params !== 'object') { + throw new Error( + `${entityName}.update expects an object of variables matching the GraphQL mutation` + ); + } + + const { id, data } = params; + + if (!id) { + throw new Error(`${entityName}.update requires an "id" field`); + } + + if (!data || typeof data !== 'object') { + throw new Error( + `${entityName}.update requires a "data" object with the fields to update` + ); + } + + const vars = { id, ...data }; + + return fn(dataConnect, vars); + + }, + }), + + + // delete + ...(ops.delete && { + delete: async (params) => { + const fn = dcSdk[ops.delete]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.delete}" not found for entity "${entityName}".` + ); + } + + if (!params || typeof params !== 'object') { + throw new Error( + `${entityName}.delete expects an object like { id }` + ); + } + + const { id } = params; + + if (!id) { + throw new Error(`${entityName}.delete requires an "id" field`); + } + + // Data Connect solo espera { id } como variables + return fn(dataConnect, { id }); + }, + }), + + + // filter + ...(ops.filter && { + filter: async (params) => { + const fn = dcSdk[ops.filter]; + if (typeof fn !== 'function') { + throw new Error( + `Data Connect operation "${ops.filter}" not found for entity "${entityName}".` + ); + } + + if (!params) { + if (ops.list) {//if no params, call to list() + const listFn = dcSdk[ops.list]; + if (typeof listFn !== 'function') { + throw new Error( + `Data Connect operation "${ops.list}" not found for entity "${entityName}".` + ); + } + return listFn(dataConnect); + } + throw new Error(`${entityName}.filter expects params or a list operation`); + } + const rawFilters = params.filters ?? params; + const variables = {}; + + for (const [key, value] of Object.entries(rawFilters)) {//cleaning undefined/null/'' values + if (value !== undefined && value !== null && value !== '') { + variables[key] = value; + } + } + + // if no valid filters, call to list() + if (Object.keys(variables).length === 0) { + if (ops.list) { + const listFn = dcSdk[ops.list]; + if (typeof listFn !== 'function') { + throw new Error( + `Data Connect operation "${ops.list}" not found for entity "${entityName}".` + ); + } + return listFn(dataConnect); + } + throw new Error(`${entityName}.filter received no valid filters and no list operation`); + } + + return fn(dataConnect, variables); + }, + }), + + }; +}); + + +// --- Main SDK Export --- +export const krowSDK = { + auth: authModule, + integrations: { + Core: coreIntegrationsModule, + }, + entities: entitiesModule, +}; From b913c647e9045963e4da7ae2e2d161f9b0952deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:02:29 -0500 Subject: [PATCH 10/15] another importan configuration for firebase --- frontend-web/src/firebase.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend-web/src/firebase.js b/frontend-web/src/firebase.js index 9f5e6fb4..26d64d59 100644 --- a/frontend-web/src/firebase.js +++ b/frontend-web/src/firebase.js @@ -1,5 +1,5 @@ // Import the functions you need from the SDKs you need -import { initializeApp } from "firebase/app"; +import { initializeApp, getApps, getApp } from "firebase/app"; import { getAuth } from "firebase/auth"; import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig } from '@dataconnect/generated'; @@ -14,7 +14,16 @@ const firebaseConfig = { appId: import.meta.env.VITE_FIREBASE_APP_ID }; -// Initialize Firebase -const app = initializeApp(firebaseConfig); +// Initialize Firebase app only once +let app; +if (getApps().length === 0) { // Check if no app is already initialized + app = initializeApp(firebaseConfig); +} else { + app = getApp(); // If already initialized, use the existing app +} + export const dataConnect = getDataConnect(app, connectorConfig); -export const auth = getAuth(app); \ No newline at end of file +export const auth = getAuth(app); + +// For debugging purposes, if you previously added this, it can stay or be removed +// window.firebaseAuth = auth; \ No newline at end of file From d2c227c9d5d26254d73b8964ef8af7457ce936ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:03:06 -0500 Subject: [PATCH 11/15] modifications for krowd sdk --- frontend-web/src/components/ui/use-toast.jsx | 15 +++- frontend-web/src/pages/AddStaff.jsx | 80 +++++++++++++++-- frontend-web/src/pages/StaffDirectory.jsx | 94 +++++++++++--------- 3 files changed, 136 insertions(+), 53 deletions(-) diff --git a/frontend-web/src/components/ui/use-toast.jsx b/frontend-web/src/components/ui/use-toast.jsx index cc33246c..a200e225 100644 --- a/frontend-web/src/components/ui/use-toast.jsx +++ b/frontend-web/src/components/ui/use-toast.jsx @@ -1,5 +1,5 @@ import React from "react" -import { base44 } from "@/api/base44Client"; +import { krowSDK } from "@/api/krowSDK"; const TOAST_LIMIT = 5 const TOAST_REMOVE_DELAY = 1000000 @@ -95,7 +95,11 @@ function dispatch(action) { // Helper function to create notification in ActivityLog instead of toast async function createNotification(title, description, variant) { try { - const user = await base44.auth.me(); + const user = await krowSDK.auth.me(); + if (!user) { + console.warn("Cannot create notification: user not authenticated."); + return; + } // Determine icon and color based on variant and title let icon_type = "check"; @@ -124,7 +128,7 @@ async function createNotification(title, description, variant) { activity_type = "staff_assigned"; } - await base44.entities.ActivityLog.create({ + const payload = { title: title.replace(/✅|❌|⚠️/g, '').trim(), description: description || "", activity_type: activity_type, @@ -132,7 +136,10 @@ async function createNotification(title, description, variant) { is_read: false, icon_type: icon_type, icon_color: icon_color, - }); + }; + + await krowSDK.entities.ActivityLog.create({ data: payload }); + } catch (error) { console.error("Failed to create notification:", error); } diff --git a/frontend-web/src/pages/AddStaff.jsx b/frontend-web/src/pages/AddStaff.jsx index 807f402b..c1a3bba5 100644 --- a/frontend-web/src/pages/AddStaff.jsx +++ b/frontend-web/src/pages/AddStaff.jsx @@ -1,26 +1,90 @@ -import React, { useState } from "react"; -import { base44 } from "@/api/base44Client"; +import React from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { createPageUrl } from "@/utils"; +import { krowSDK } from "@/api/krowSDK"; import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; import { ArrowLeft } from "lucide-react"; import StaffForm from "@/components/staff/StaffForm"; export default function AddStaff() { const navigate = useNavigate(); const queryClient = useQueryClient(); + const { toast } = useToast(); const createStaffMutation = useMutation({ - mutationFn: (staffData) => base44.entities.Staff.create(staffData), + mutationFn: (staffPayload) => krowSDK.entities.Staff.create({ data: staffPayload }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['staff'] }); - navigate(createPageUrl("Dashboard")); + toast({ + title: "✅ Staff Member Added", + description: "The new staff member has been successfully created.", + }); + navigate(createPageUrl("StaffDirectory")); + }, + onError: (error) => { + toast({ + title: "❌ Error Creating Staff", + description: error.message || "An unknown error occurred.", + variant: "destructive", + }); }, }); - const handleSubmit = (staffData) => { - createStaffMutation.mutate(staffData); + const handleSubmit = (formData) => { + // 1. Map snake_case from form to camelCase for GraphQL + // 2. Transform enum values to uppercase + // 3. Add required fields not in the form + // 4. Filter out fields not in the mutation + const employmentTypeMap = { + "Full Time": "FULL_TIME", + "Part Time": "PART_TIME", + "On call": "ON_CALL", + "Weekends": "WEEKENDS", + "Specific Days": "SPECIFIC_DAYS", + "Seasonal": "SEASONAL", + "Medical Leave": "MEDICAL_LEAVE", + }; + + const englishLevelMap = { + "Fluent": "FLUENT", + "Intermediate": "INTERMEDIATE", + "Basic": "BASIC", + "None": "NONE", + }; + + const payload = { + // --- Fields from error messages --- + employeeName: formData.employee_name, + employmentType: employmentTypeMap[formData.employment_type], + english: englishLevelMap[formData.english], + backgroundCheckStatus: 'NOT_REQUIRED', // Default as it's missing from form + + // --- Other likely fields (from form) --- + contactNumber: formData.contact_number, + hubLocation: formData.hub_location, + profileType: formData.profile_type, + reliabilityScore: parseInt(formData.reliability_score) || 100, + + // --- Fields from form that might match schema --- + email: formData.email, + position: formData.position, + department: formData.department, + manager: formData.manager, + rate: parseFloat(formData.rate) || 0, + notes: formData.notes, + rating: parseFloat(formData.rating) || 0, + }; + + // Remove any keys with undefined values to keep the payload clean + Object.keys(payload).forEach(key => { + if (payload[key] === undefined || payload[key] === null) { + delete payload[key]; + } + }); + + createStaffMutation.mutate(payload); }; return ( @@ -29,11 +93,11 @@ export default function AddStaff() {

Add New Staff Member

Fill in the details to add a new team member

diff --git a/frontend-web/src/pages/StaffDirectory.jsx b/frontend-web/src/pages/StaffDirectory.jsx index 29fda88f..88ce6f0c 100644 --- a/frontend-web/src/pages/StaffDirectory.jsx +++ b/frontend-web/src/pages/StaffDirectory.jsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; -import { base44 } from "@/api/base44Client"; import { useQuery } from "@tanstack/react-query"; import { Link } from "react-router-dom"; import { createPageUrl } from "@/utils"; +import { krowSDK } from "@/api/krowSDK"; +import { useAuth } from "@/hooks/useAuth"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { UserPlus, Users, LayoutGrid, List as ListIcon, Phone, MapPin, Calendar, Star } from "lucide-react"; import FilterBar from "@/components/staff/FilterBar"; -import StaffCard from "@/components/staff/StaffCard"; import EmployeeCard from "@/components/staff/EmployeeCard"; import PageHeader from "@/components/common/PageHeader"; @@ -18,50 +18,61 @@ export default function StaffDirectory() { const [locationFilter, setLocationFilter] = useState("all"); const [viewMode, setViewMode] = useState("grid"); // "grid" or "list" - const { data: user } = useQuery({ - queryKey: ['current-user'], - queryFn: () => base44.auth.me(), + const { user: authUser } = useAuth(); // Firebase auth user + + const { data: krowUser, isLoading: isLoadingUser } = useQuery({ + queryKey: ['krow-user', authUser?.uid], + queryFn: () => krowSDK.entities.User.get({ id: authUser.uid }), // Changed from .filter() to .get() + enabled: !!authUser?.uid, + select: (response) => response?.data?.user, // Adjusted to get single user object }); - const { data: staff, isLoading } = useQuery({ + const { data: staff, isLoading: isLoadingStaff } = useQuery({ queryKey: ['staff'], - queryFn: () => base44.entities.Staff.list('-created_date'), + queryFn: () => krowSDK.entities.Staff.list(), initialData: [], + select: (response) => { + // The API returns { data: { staffs: [...] } }, so we need to access the nested array. + if (response && response.data && Array.isArray(response.data.staffs)) { + return response.data.staffs; + } + return []; // Return empty array if the structure is not as expected. + }, }); const { data: events } = useQuery({ queryKey: ['events-for-staff-filter'], - queryFn: () => base44.entities.Event.list(), - initialData: [], - enabled: !!user + queryFn: () => krowSDK.entities.Event.list(), + initialData: { data: [] }, + enabled: !!krowUser, + select: (data) => data.data || [], }); const visibleStaff = React.useMemo(() => { - const userRole = user?.user_role || user?.role; - - if (['admin', 'procurement'].includes(userRole)) { + if (!krowUser || !staff) return []; + const userRole = krowUser.user_role || krowUser.role; + + if (['admin', 'procurement', 'operator', 'sector'].includes(userRole)) { return staff; } - - if (['operator', 'sector'].includes(userRole)) { - return staff; - } - + if (userRole === 'vendor') { - return staff.filter(s => - s.vendor_id === user?.id || - s.vendor_name === user?.company_name || - s.created_by === user?.email + return staff.filter(s => + s.vendor_id === krowUser.id || + s.vendor_name === krowUser.company_name || + //s.created_by === krowUser.email + e.created_by === krowUser.id ); } - + if (userRole === 'client') { - const clientEvents = events.filter(e => - e.client_email === user?.email || - e.business_name === user?.company_name || - e.created_by === user?.email + const clientEvents = events.filter(e => + e.client_email === krowUser.email || + e.business_name === krowUser.company_name || + //e.created_by === krowUser.email + e.created_by === krowUser.id ); - + const assignedStaffIds = new Set(); clientEvents.forEach(event => { if (event.assigned_staff) { @@ -72,36 +83,37 @@ export default function StaffDirectory() { }); } }); - + return staff.filter(s => assignedStaffIds.has(s.id)); } - + if (userRole === 'workforce') { return staff; } - + return staff; - }, [staff, user, events]); + }, [staff, krowUser, events]); const uniqueDepartments = [...new Set(visibleStaff.map(s => s.department).filter(Boolean))]; const uniqueLocations = [...new Set(visibleStaff.map(s => s.hub_location).filter(Boolean))]; const filteredStaff = visibleStaff.filter(member => { - const matchesSearch = !searchTerm || + const matchesSearch = !searchTerm || member.employee_name?.toLowerCase().includes(searchTerm.toLowerCase()) || member.position?.toLowerCase().includes(searchTerm.toLowerCase()) || member.manager?.toLowerCase().includes(searchTerm.toLowerCase()); - + const matchesDepartment = departmentFilter === "all" || member.department === departmentFilter; const matchesLocation = locationFilter === "all" || member.hub_location === locationFilter; - + return matchesSearch && matchesDepartment && matchesLocation; }); - const canAddStaff = ['admin', 'procurement', 'operator', 'sector', 'vendor'].includes(user?.user_role || user?.role); + const canAddStaff = krowUser && ['admin', 'procurement', 'operator', 'sector', 'vendor'].includes(krowUser.user_role || krowUser.role); + const isLoading = isLoadingStaff || isLoadingUser; const getPageTitle = () => { - const userRole = user?.user_role || user?.role; + const userRole = krowUser?.user_role || krowUser?.role; if (userRole === 'vendor') return "My Staff Directory"; if (userRole === 'client') return "Event Staff Directory"; if (userRole === 'workforce') return "Team Directory"; @@ -109,14 +121,14 @@ export default function StaffDirectory() { }; const getPageSubtitle = () => { - const userRole = user?.user_role || user?.role; + const userRole = krowUser?.user_role || krowUser?.role; if (userRole === 'vendor') return `${filteredStaff.length} of your staff members`; if (userRole === 'client') return `${filteredStaff.length} staff assigned to your events`; if (userRole === 'workforce') return `${filteredStaff.length} team members`; return `${filteredStaff.length} ${filteredStaff.length === 1 ? 'member' : 'members'} found`; }; - - const getCoverageColor = (percentage) => { + + const getCoverageColor = (percentage) => { if (!percentage) return "bg-red-100 text-red-700"; if (percentage >= 90) return "bg-green-100 text-green-700"; if (percentage >= 50) return "bg-yellow-100 text-yellow-700"; @@ -126,7 +138,7 @@ export default function StaffDirectory() { return (
- Date: Tue, 2 Dec 2025 09:18:01 -0500 Subject: [PATCH 12/15] new version of staff for the actual front --- dataconnect/connector/staff/mutations.gql | 8 ++++++++ dataconnect/connector/staff/queries.gql | 6 ++++++ dataconnect/schema/staff.gql | 20 +++++++++++--------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/dataconnect/connector/staff/mutations.gql b/dataconnect/connector/staff/mutations.gql index 20d26162..64f2954a 100644 --- a/dataconnect/connector/staff/mutations.gql +++ b/dataconnect/connector/staff/mutations.gql @@ -12,9 +12,11 @@ mutation CreateStaff( $profileType: ProfileType, $employmentType: EmploymentType!, $english: EnglishLevel, + $rate: Float, $rating: Float, $reliabilityScore: Int, $backgroundCheckStatus: BackgroundCheckStatus! + $notes: String ) @auth(level: USER) { staff_insert( data: { @@ -31,9 +33,11 @@ mutation CreateStaff( profileType: $profileType employmentType: $employmentType english: $english + rate: $rate rating: $rating reliabilityScore: $reliabilityScore backgroundCheckStatus: $backgroundCheckStatus + notes: $notes } ) } @@ -53,9 +57,11 @@ mutation UpdateStaff( $profileType: ProfileType, $employmentType: EmploymentType, $english: EnglishLevel, + $rate: Float, $rating: Float, $reliabilityScore: Int, $backgroundCheckStatus: BackgroundCheckStatus + $notes: String ) @auth(level: USER) { staff_update( id: $id, @@ -73,9 +79,11 @@ mutation UpdateStaff( profileType: $profileType employmentType: $employmentType english: $english + rate: $rate rating: $rating reliabilityScore: $reliabilityScore backgroundCheckStatus: $backgroundCheckStatus + notes: $notes } ) } diff --git a/dataconnect/connector/staff/queries.gql b/dataconnect/connector/staff/queries.gql index da35030a..87ed5235 100644 --- a/dataconnect/connector/staff/queries.gql +++ b/dataconnect/connector/staff/queries.gql @@ -14,9 +14,11 @@ query listStaff @auth(level: USER) { profileType employmentType english + rate rating reliabilityScore backgroundCheckStatus + notes } } @@ -38,9 +40,11 @@ query getStaffById( profileType employmentType english + rate rating reliabilityScore backgroundCheckStatus + notes } } @@ -70,8 +74,10 @@ query filterStaff( position employmentType english + rate rating reliabilityScore backgroundCheckStatus + notes } } diff --git a/dataconnect/schema/staff.gql b/dataconnect/schema/staff.gql index 3a0bab4e..a4aaf878 100644 --- a/dataconnect/schema/staff.gql +++ b/dataconnect/schema/staff.gql @@ -42,22 +42,24 @@ enum BackgroundCheckStatus { type Staff @table(name: "staffs") { id: UUID! @default(expr: "uuidV4()") - employeeName: String! - vendorId: UUID # vendor_id (FK lógica a Vendor.id) - vendorName: String + employeeName: String! @col(name: "employee_name") + vendorId: UUID @col(name: "vendor_id") # vendor_id (FK lógica a Vendor.id) + vendorName: String @col(name: "vendor_name") manager: String - contactNumber: String + contactNumber: String @col(name: "contact_number") email: String department: StaffDepartment - hubLocation: String + hubLocation: String @col(name: "hub_location") track: String position: String - profileType: ProfileType - employmentType: EmploymentType! + profileType: ProfileType @col(name: "profile_type") + employmentType: EmploymentType! @col(name: "employment_type") english: EnglishLevel + rate: Float rating: Float - reliabilityScore: Int - backgroundCheckStatus: BackgroundCheckStatus! # background_check_status + reliabilityScore: Int @col(name: "reliability_score") + backgroundCheckStatus: BackgroundCheckStatus! @col(name: "background_check_status") + notes: String createdDate: Timestamp @default(expr: "request.time") updatedDate: Timestamp @default(expr: "request.time") createdBy: String @default(expr: "auth.uid") From b76cde086ac2fcf138a5648b4e15c016c92c15dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:19:31 -0500 Subject: [PATCH 13/15] modifying authModule int krowSDK --- frontend-web/src/api/krowSDK.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend-web/src/api/krowSDK.js b/frontend-web/src/api/krowSDK.js index 45d6c64f..f1923ce8 100644 --- a/frontend-web/src/api/krowSDK.js +++ b/frontend-web/src/api/krowSDK.js @@ -11,8 +11,33 @@ const authModule = { * @returns {Promise} The user profile. */ me: async () => { - const { data } = await apiClient.get('/auth/me'); - return data; + // 1. Firebase auth user + const fbUser = auth.currentUser; + + if (!fbUser) { + return null; // NO ESTÁ LOGGEADO + } + + // 2. Attempt to load matching Krow User from DataConnect + // (because your Krow user metadata is stored in the "users" table) + let krowUser = null; + try { + const response = await dcSdk.getUser(dataConnect, { id: fbUser.uid }); + krowUser = response.data?.user || null; + } catch (err) { + console.warn("Krow user not found in DataConnect, returning Firebase-only info."); + } + + // 3. Build unified "me" object + return { + id: fbUser.uid, + email: fbUser.email, + fullName: krowUser?.fullName || fbUser.displayName || null, + role: krowUser?.role || "user", + user_role: krowUser?.userRole || null, + firebase: fbUser, + krow: krowUser + }; }, /** From bc5c08255f6cfa47632e2bfdf1fb2ceac3bbaffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:20:00 -0500 Subject: [PATCH 14/15] adding modifications for the front --- frontend-web/src/components/ui/use-toast.jsx | 1 + frontend-web/src/pages/StaffDirectory.jsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend-web/src/components/ui/use-toast.jsx b/frontend-web/src/components/ui/use-toast.jsx index a200e225..5ad1caad 100644 --- a/frontend-web/src/components/ui/use-toast.jsx +++ b/frontend-web/src/components/ui/use-toast.jsx @@ -96,6 +96,7 @@ function dispatch(action) { async function createNotification(title, description, variant) { try { const user = await krowSDK.auth.me(); + if (!user) { console.warn("Cannot create notification: user not authenticated."); return; diff --git a/frontend-web/src/pages/StaffDirectory.jsx b/frontend-web/src/pages/StaffDirectory.jsx index 88ce6f0c..4751c3e2 100644 --- a/frontend-web/src/pages/StaffDirectory.jsx +++ b/frontend-web/src/pages/StaffDirectory.jsx @@ -52,7 +52,7 @@ export default function StaffDirectory() { if (!krowUser || !staff) return []; const userRole = krowUser.user_role || krowUser.role; - if (['admin', 'procurement', 'operator', 'sector'].includes(userRole)) { + if (['admin', 'procurement', 'operator', 'sector'].includes(userRole.toLowerCase())) { return staff; } @@ -109,7 +109,7 @@ export default function StaffDirectory() { return matchesSearch && matchesDepartment && matchesLocation; }); - const canAddStaff = krowUser && ['admin', 'procurement', 'operator', 'sector', 'vendor'].includes(krowUser.user_role || krowUser.role); + const canAddStaff = krowUser && ['admin', 'procurement', 'operator', 'sector', 'vendor'].includes((krowUser.user_role || krowUser.role || '').toLowerCase()); const isLoading = isLoadingStaff || isLoadingUser; const getPageTitle = () => { From a01a14e3538f60f8a4c2dc02d677216d2994d831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:38:07 -0500 Subject: [PATCH 15/15] recomendations for front ia --- .../dataconnect-naming-and-enum-findings.md | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 frontend-web/dataconnect-naming-and-enum-findings.md diff --git a/frontend-web/dataconnect-naming-and-enum-findings.md b/frontend-web/dataconnect-naming-and-enum-findings.md new file mode 100644 index 00000000..d2c11760 --- /dev/null +++ b/frontend-web/dataconnect-naming-and-enum-findings.md @@ -0,0 +1,253 @@ +# DataConnect – Inconsistencies between the Base44 frontend and the backend (Firebase Data Connect + PostgreSQL) +**Author:** José Salazar +**Date:** Dec 2025 + +--- + +## 📌 Purpose of this document + +Document all findings during the integration of the new backend +(**Firebase Data Connect + PostgreSQL**) with the frontend generated by **Base44 AI**. + +This document summarizes: + +- Issues found +- camelCase vs snake_case inconsistencies +- Enum inconsistencies (uppercase/lowercase and dashes) +- Differences between what DataConnect returns vs what the frontend expects +- Recommended fixes +- Suggestions for Base44 AI to update its model +- Impact on queries, mutations, and the generated SDK + +--- + +## 1️⃣ Enums – Different formats between Front and Backend + +### Observation + +In the frontend, enum values are in mixed formats, for example: + +- UPPERCASE: SKILLED +- camelCase: fullTime + +In the backend (DataConnect schema), enums are defined only in UPPERCASE, for example: + +- FULL_TIME +- CROSS_TRAINED +- NOT_REQUIRED +- PENDING + +DataConnect only accepts these exact values. + +### Problem + +When the frontend sends: + +- "crossTrained" instead of CROSS_TRAINED +- "fluent" instead of FLUENT + +### Impact + +- Mutations can fail or return enum validation errors. +- Filters using enums return no results. +- Behavior changes depending on how Base44 AI generated the object. + +### Recommendation + +- Define a single standard: **ALL enums must be UPPERCASE** on the frontend and backend. +- Before sending to the backend, normalize enum values to uppercase. + +### Suggestion for Base44 AI + +- Adjust models so they always generate enums in UPPERCASE. + +--- + +## 2️⃣ Enums with dashes (“-”) – Not valid in GraphQL + +### Observation + +In the legacy frontend, some enum values contain dashes, for example: + +- CUSTOMER-SERVICE +- CROSS-TRAINED +- PART-TIME + +But in GraphQL enums only allow letters, numbers, and underscores. +The backend had to define them as: + +- CUSTOMER_SERVICE +- CROSS_TRAINED +- PART_TIME + +### Problem + +When the frontend sends "CUSTOMER-SERVICE" or "CROSS-TRAINED": + +- The backend expects CUSTOMER_SERVICE or CROSS_TRAINED. +- There is no match between the frontend value and the DataConnect enum. + +### Impact + +- Enum filters return nothing. +- Mutations fail when trying to save invalid enum values. +- Compatibility between the Base44 model and the DataConnect schema breaks. + +### Recommendation + +- Standardize all enums to UPPERCASE SNAKE_CASE (e.g., CUSTOMER_SERVICE). +- Never use dashes “-” in enum values. + +### Suggestion for Base44 AI + +- Update models so enum values are always generated as + UPPERCASE_WITH_UNDERSCORE (e.g., CUSTOMER_SERVICE), without dashes. + +--- + +## 3️⃣ Field names – Front in snake_case vs DataConnect in camelCase + +### Observation + +The original Base44 frontend uses snake_case field names, for example: + +- contact_number +- vendor_id +- background_check_status +- hub_location + +In DataConnect the schema is camelCase, and although you can map to the actual PostgreSQL column using @col, the GraphQL type remains camelCase, for example: + +- contactNumber (mapped to "contact_number" in Postgres) +- vendorId (mapped to "vendor_id") +- backgroundCheckStatus (mapped to "background_check_status") +- hubLocation (mapped to "hub_location") + +Meaning: + +- In the database (PostgreSQL) names remain snake_case. +- In DataConnect and the SDK they are exposed as camelCase. + +### Problem + +The frontend still expects/reads fields like contact_number, but the SDK returns contactNumber. +A similar issue happens when the frontend sends payloads in snake_case: + +- The GraphQL schema does not recognize contact_number. +- It only accepts contactNumber. + +### Impact + +- UI fails to show data because it reads keys that don’t exist (snake_case). +- Mutations fail or ignore fields due to mismatched names. +- Filters with snake_case are invalid in GraphQL. + +### Recommendation + +- Agree that **all communication with DataConnect (frontend + SDK) uses camelCase**. +- Keep snake_case only at PostgreSQL level using @col, for example: + + employeeName: String @col(name: "employee_name") + +Thus: + +- Frontend / SDK / GraphQL → camelCase (employeeName) +- PostgreSQL → snake_case (employee_name) + +### Suggestion for Base44 AI + +- Adjust generated frontend code so it uses camelCase when consuming the new backend. +- If Supabase or another backend is still used, document all mappings clearly. + +--- + +## 4️⃣ Fields used by the frontend but not listed in API Spec v3 + +### Observation + +During integration we found that Base44 frontend uses fields not defined in the official document: + +Reference file: +docs/03-backend-api-specification-v3.md + +Examples in the Staff entity: + +The frontend sends and displays fields like: + +- notes +- rate + +But these fields were not defined in the original v3 specification for Staff. + +### Problem + +- The frontend assumes these fields exist because the old database had them. +- The DataConnect schema does not include them. +- Sending these values in mutations causes validation errors. + +### Impact + +- Inconsistency between what the UI shows/edits and what is actually persisted. +- Risk of losing data the user believes is being saved. +- Hard to maintain a 1:1 mapping between the previous Base44 model and the new backend. + +### Recommendation + +- Validate which fields should truly exist for each entity (e.g., Staff). +- Align these three items: + 1. API Spec v3 + 2. DataConnect Schema + 3. Base44 Frontend + +- If a field is no longer needed, remove it from the frontend. +- If important, add it formally to the API Spec and to the DataConnect schema. + +--- + +## 5️⃣ DataConnect vs Front – Observed behavior + +1. DataConnect: + - Always exposes fields in camelCase. + - Enforces enum restrictions exactly as defined (UPPERCASE, no dashes). + - Allows mapping to Postgres column names using @col, but GraphQL names remain camelCase. + +2. Base44 Frontend: + - Uses snake_case in many areas. + - Uses enums in mixed formats (uppercase, camelCase, with dashes). + - Contains extra fields not included in API Spec v3. + +--- + +## 6️⃣ Suggested fixes (personal criteria) + +1. Enums + - Standardize to UPPERCASE_SNAKE_CASE for all enum values. + - Apply a normalization layer in the frontend to convert any format into the official one before hitting the backend. + +2. Field names + - Migrate the frontend to camelCase for any interaction with DataConnect. + - Keep snake_case only at the database layer using @col. + +3. Extra fields + - Review all fields the frontend sends and compare with API Spec v3. + - Remove or add fields depending on what becomes the “source of truth” (Spec v3). + +4. Documentation + - Keep this file updated as the Base44 → DataConnect migration reference. + - Add valid payload examples for each entity (Staff, Vendor, Invoice, etc.). + +--- + +## 7️⃣ Summary + +- Always generate: + - Enums: UPPERCASE_SNAKE_CASE (e.g., FULL_TIME, CUSTOMER_SERVICE). + - Fields: camelCase (e.g., contactNumber, hubLocation, backgroundCheckStatus). + +- Avoid: + - Dashes “-” in enum values. + - Spaces in enum values. + +--- + +This document captures the current findings and serves as a guide to fully align the Base44 frontend with the backend based on Firebase Data Connect + PostgreSQL.