diff --git a/.gitignore b/.gitignore
index 4efcf77fe..febee6765 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,4 +85,7 @@ build/
docs/
# session code
-private/
\ No newline at end of file
+private/
+
+#prisma
+/generated/prisma
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..b8ffd7075
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+22.15.0
diff --git a/frontend/package.json b/frontend/package.json
index 1bd46950b..0a3fe8d98 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,10 +5,11 @@
"main": "index.js",
"dependencies": {
"@carriage-web/shared": "workspace:*",
- "@tailwindcss/vite": "^4.2.2"
+ "react-scripts": "^5.0.1"
},
"scripts": {
"dev": "vite",
+ "start": "vite",
"build": "vite build",
"preview": "vite preview",
"type-check": "tsc --project tsconfig.json --pretty --noEmit"
@@ -34,6 +35,7 @@
"@mui/utils": "^7.3.6",
"@mui/x-date-pickers": "^7.23.1",
"@react-aria/utils": "^3.25.2",
+ "@tailwindcss/vite": "^4.2.2",
"@types/crypto-js": "^4.2.2",
"@types/google.maps": "^3.58.1",
"@types/node": "^22.5.5",
diff --git a/frontend/src/components/EmployeeCards/EmployeeCards.tsx b/frontend/src/components/EmployeeCards/EmployeeCards.tsx
index 026029e79..f0d10d668 100644
--- a/frontend/src/components/EmployeeCards/EmployeeCards.tsx
+++ b/frontend/src/components/EmployeeCards/EmployeeCards.tsx
@@ -2,8 +2,7 @@ import { useNavigate } from 'react-router-dom';
import Card, { CardInfo } from '../Card/Card';
import styles from './employeecards.module.css';
import { phone, wheel, user } from '../../icons/userInfo/index';
-import { AdminType } from '@carriage-web/shared/types/admin';
-import { DriverType } from '@carriage-web/shared/types/driver';
+import { EmployeeType } from '@carriage-web/shared/types/employee';
const formatPhone = (phoneNumber: string | undefined) => {
if (phoneNumber !== undefined) {
@@ -17,19 +16,9 @@ const formatPhone = (phoneNumber: string | undefined) => {
}
};
-type Employee = AdminType | DriverType;
-
-function isAdmin(employee: Employee): employee is AdminType {
- return 'isDriver' in employee;
-}
-
-function isDriver(employee: Employee): employee is DriverType {
- return 'availability' in employee && !('isDriver' in employee);
-}
-
type EmployeeCardProps = {
id: string;
- employee: Employee;
+ employee: EmployeeType;
};
const EmployeeCard = ({ id, employee }: EmployeeCardProps) => {
@@ -37,14 +26,9 @@ const EmployeeCard = ({ id, employee }: EmployeeCardProps) => {
const netId = employee.email.split('@')[0];
const fmtPhone = formatPhone(employee.phoneNumber);
- // Determine if employee is admin, driver, or both
- const adminEmployee = isAdmin(employee);
- const driverEmployee = isDriver(employee);
- const isBoth = adminEmployee && employee.isDriver;
-
const roles = (): string => {
- if (isBoth) return 'Admin • Driver';
- if (adminEmployee) return 'Admin';
+ if (employee.isAdmin && employee.isDriver) return 'Admin • Driver';
+ if (employee.isAdmin) return 'Admin';
return 'Driver';
};
@@ -68,8 +52,8 @@ const EmployeeCard = ({ id, employee }: EmployeeCardProps) => {
{fmtPhone}
{roles()}
@@ -79,7 +63,7 @@ const EmployeeCard = ({ id, employee }: EmployeeCardProps) => {
};
type EmployeeCardsProps = {
- employees: Employee[];
+ employees: EmployeeType[];
};
const EmployeeCards = ({ employees }: EmployeeCardsProps) => {
diff --git a/frontend/src/components/EmployeeModal/EmployeeModal.tsx b/frontend/src/components/EmployeeModal/EmployeeModal.tsx
index d8c0c5480..8a96b3df3 100644
--- a/frontend/src/components/EmployeeModal/EmployeeModal.tsx
+++ b/frontend/src/components/EmployeeModal/EmployeeModal.tsx
@@ -13,9 +13,10 @@ import { useEmployees } from '../../context/EmployeesContext';
import { useToast, ToastStatus } from '../../context/toastContext';
import axios from '../../util/axios';
import { extractNetIdFromEmail } from 'util/userUtils';
+import { EmployeeType } from '@carriage-web/shared/types/employee';
type AdminData = {
- type: string[];
+ adminRoles: string[];
isDriver: boolean;
};
@@ -37,23 +38,26 @@ type EmployeeEntity = {
photoLink?: string;
};
-// both for formating to current api data expections
-function extractAdminData(employeeData: EmployeeEntity) {
+// both for formatting to current api data expectations
+function extractAdminData(
+ employeeData: EmployeeEntity
+): Omit {
return {
firstName: employeeData.firstName,
lastName: employeeData.lastName,
- type: (employeeData.admin?.type || []) as (
- | 'sds-admin'
- | 'redrunner-admin'
- )[],
+ adminRoles: employeeData.admin?.adminRoles || [],
+ isAdmin: true,
isDriver: employeeData.admin?.isDriver || false,
phoneNumber: employeeData.phoneNumber,
email: employeeData.email,
photoLink: employeeData.photoLink,
+ availability: employeeData.driver?.availability || [],
};
}
-function extractDriverData(employeeData: EmployeeEntity) {
+function extractDriverData(
+ employeeData: EmployeeEntity
+): Partial {
return {
firstName: employeeData.firstName,
lastName: employeeData.lastName,
@@ -62,6 +66,7 @@ function extractDriverData(employeeData: EmployeeEntity) {
joinDate: employeeData.driver?.startDate,
email: employeeData.email,
photoLink: employeeData.photoLink,
+ isDriver: true,
};
}
@@ -93,13 +98,11 @@ const EmployeeModal = ({
// Initialize form and roles when modal opens or existing employee changes
React.useEffect(() => {
if (existingEmployee && isOpen) {
- // Initialize roles
+ // Initialize roles, normalizing Prisma enum values (SDS_ADMIN → sds-admin)
+ const normalizeRole = (r: string) => r.toLowerCase().replace(/_/g, '-');
const roles: string[] = [];
- if (existingEmployee.admin) {
- // Add admin roles
- if (existingEmployee.admin.type) {
- roles.push(...existingEmployee.admin.type);
- }
+ if (existingEmployee.admin?.adminRoles) {
+ roles.push(...existingEmployee.admin.adminRoles.map(normalizeRole));
}
if (existingEmployee.driver) {
roles.push('driver');
@@ -131,8 +134,9 @@ const EmployeeModal = ({
const closeModal = () => {
methods.clearErrors();
- setImageBase64(''); // Reset image state
- setIsUploadingImage(false); // Reset upload state
+ setImageBase64('');
+ setIsUploadingImage(false);
+ setSelectedRole([]);
setIsOpen(false);
};
@@ -178,7 +182,9 @@ const EmployeeModal = ({
switch (endpoint) {
case '/api/drivers':
// Use optimistic create from context
- await createDriver(extractDriverData(employeeData));
+ await createDriver(
+ extractDriverData(employeeData) as Omit
+ );
res = employeeData; // The context will handle server response and ID assignment
break;
case '/api/admins':
@@ -330,7 +336,7 @@ const EmployeeModal = ({
if (hasAdmin) {
admin_data = {
- type: selectedRoles.filter((role) => role !== 'driver'),
+ adminRoles: selectedRoles.filter((role) => role !== 'driver'),
isDriver: hasDriver,
};
}
@@ -436,8 +442,13 @@ const EmployeeModal = ({
phone={existingEmployee?.phoneNumber}
/>
+
+
{(selectedRoles.includes('driver') ||
- existingEmployee?.driver?.availability) && (
+ existingEmployee?.driver != null) && (
<>
)}
-
-
diff --git a/frontend/src/components/ResponsiveRideCard.tsx b/frontend/src/components/ResponsiveRideCard.tsx
index 1bfacb87f..d4776d871 100644
--- a/frontend/src/components/ResponsiveRideCard.tsx
+++ b/frontend/src/components/ResponsiveRideCard.tsx
@@ -152,7 +152,7 @@ const ResponsiveRideCard: FC = ({
Driver
- {ride.driver !== undefined
+ {ride.driver?.firstName
? `${ride.driver.firstName} ${ride.driver.lastName}`
: 'Not Assigned'}
@@ -202,7 +202,7 @@ const ResponsiveRideCard: FC = ({
defaultZoom={13}
gestureHandling="greedy"
disableDefaultUI
- mapId={process.env.VITE_GOOGLE_MAPS_MAP_ID}
+ mapId={import.meta.env.VITE_GOOGLE_MAPS_MAP_ID}
>
= ({
const [contactAdminOpen, setContactAdminOpen] = useState(false);
const [updating, setUpdating] = useState(false);
const [saving, setSaving] = useState(false);
+ const [scopeDialogMode, setScopeDialogMode] = useState<
+ 'save' | 'cancel' | null
+ >(null);
const ride = editedRide!; // We know this exists from the context
const rideCompleted = ride.status === Status.COMPLETED;
@@ -91,32 +94,26 @@ const RideActions: React.FC = ({
};
const handleCancel = () => {
- setCancelConfirmOpen(true);
- };
-
- const handleCancelConfirm = async () => {
- // Check for recurring rides (not supported yet)
- if (ride.isRecurring) {
- showToast('Recurring ride deletion not supported yet', ToastStatus.ERROR);
- return;
+ // Recurring rides need scope selection before cancelling
+ if (ride.recurrenceId) {
+ setScopeDialogMode('cancel');
+ } else {
+ setCancelConfirmOpen(true);
}
+ };
+ const handleCancelConfirm = async (scope?: 'single' | 'future') => {
try {
- // Call the DELETE endpoint like DeleteOrEditTypeModal does
- await axios.delete(`/api/rides/${ride.id}`);
+ const url = scope
+ ? `/api/rides/${ride.id}?scope=${scope}`
+ : `/api/rides/${ride.id}`;
+ await axios.delete(url);
- // Close the cancel confirmation modal
setCancelConfirmOpen(false);
+ setScopeDialogMode(null);
- // Close the main ride details dialog since the ride no longer exists
- if (onClose) {
- onClose();
- }
-
- // Refresh the rides data
+ if (onClose) onClose();
refreshRides();
-
- // Show success message
showToast('Ride Cancelled', ToastStatus.SUCCESS);
} catch (error) {
console.error('Failed to cancel ride:', error);
@@ -126,34 +123,35 @@ const RideActions: React.FC = ({
const handleEdit = () => {
if (isEditing) {
- // Save changes
handleSave();
} else {
- // Start editing
startEditing();
}
};
- const handleSave = async () => {
+ const handleSave = async (scope?: 'single' | 'future') => {
+ // If ride is recurring and no scope decided yet, show scope dialog
+ if (ride.recurrenceId && scope === undefined) {
+ setScopeDialogMode('save');
+ return;
+ }
+
setSaving(true);
try {
- const success = await saveChanges();
+ const success = await saveChanges(scope);
if (success) {
const message = isNewRide(ride)
? 'Ride created successfully'
: 'Ride saved successfully';
showToast(message, ToastStatus.SUCCESS);
- // Only refresh rides if the ride is on the current date being displayed in the context
const rideDate = new Date(ride.startTime).toDateString();
const contextDate = curDate.toDateString();
if (rideDate === contextDate) {
refreshRides();
}
- if (onClose) {
- onClose(); // Close modal after creating new ride
- }
+ if (onClose) onClose();
} else {
const message = isNewRide(ride)
? 'Failed to create ride'
@@ -168,6 +166,7 @@ const RideActions: React.FC = ({
showToast(message, ToastStatus.ERROR);
} finally {
setSaving(false);
+ setScopeDialogMode(null);
}
};
@@ -422,7 +421,7 @@ const RideActions: React.FC = ({
+ {/* Recurring Scope Dialog — shown when editing or cancelling a recurring ride */}
+
+
{/* Contact Admin Modal */}