Samaritan Inn Scheduling App is a web application serving the residents and staff of Samaritan Inn, a nonprofit homeless shelter dedicated to community support. Staff members can create, schedule, edit, and remove announcements to keep everyone informed and organized. Residents can register for life skills classes, book appointments, request curfew extensions, and manage personal schedules through an integrated calendar. In addition, residents can submit pass forms to request extended curfew or time away from the shelter. Residents can see if their requests have been approved or denied by staff. Staff can approve or deny residents' requests and see past forms. The platform leverages Next.js, Prisma, and SQLite to deliver a secure, scalable, and user-friendly experience.
- Features
- Tech Stack
- Getting Started
- Project Structure
- Database Management
- Authentication
- Salesforce Integration and APIs
- Deployment
- Contributing
- License
- Residents
- View announcements posted by staff.
- Schedule life skills classes to enhance their personal development.
- Request curfew extensions.
- Manage their schedules using a personal calendar to organize appointments and busy times.
- Staff
- Create, post, and schedule announcements for residents.
- Manage life skills classes, including scheduling and capacity limits.
- Deny or approve resident passes.
- Staff can create, edit, and delete announcements.
- Announcements include timestamps and author details.
- Residents
- Residents can request curfew extensions through the platform.
- Residents can see if their requests have been approved or denied by staff.
- Staff
- Staff can approve or deny residents' requests
- Staff can see past forms
- Secure login and signup using NextAuth.js.
- Role-based access for staff and residents.
- Optimized for both desktop and mobile devices.
- Frontend: Next.js (React Framework)
- Backend: Node.js with Next.js API routes
- Database: SQLite with Prisma ORM
- Authentication: NextAuth.js
- Styling: Tailwind CSS
- Node.js (v18 or higher)
- npm or Yarn
- SQLite (comes pre-installed with Prisma)
- Clone the repository:
git clone https://github.com/your-username/samaritan-inn.git cd samaritan-inn - Install dependencies:
npm install- (create
.envfile and add the required information) npx prisma migrate devnpx prisma generate
- Configure environment variables:
Create a
.envfile in the root directory with:DATABASE_URL="file:./dev.db" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="your-secret-key-for-jwt-encryption"
- Start the development server:
npm run dev # or yarn dev - Open your browser and navigate to
http://localhost:3000.
βββ .env
βββ .gitignore
βββ eslint.config.mjs
βββ next-env.d.ts
βββ next.config.ts
βββ package.json
βββ postcss.config.mjs
βββ README.md
βββ tailwind.config.ts
βββ tsconfig.json
βββ prisma
β βββ schema.prisma
β βββ dev.db
β βββ migrations
βββ public
β βββ file.svg
β βββ globe.svg
β βββ next.svg
β βββ vercel.svg
β βββ window.svg
βββ src
β βββ app
β β βββ favicon.ico
β β βββ globals.css
β β βββ layout.tsx
β β βββ page.tsx
β β βββ admin-forms
β β βββ announcements
β β βββ api
β β β βββ announcements
β β β βββ auth
β β β βββ login
β β β βββ register
β β βββ auth-status
β β βββ calendar-form
β β βββ caseworker
β β βββ curfew
β β βββ dashboard
β β βββ homepage
β β βββ login
β β βββ pass-form
β β βββ signup
β β βββ unauthorized
β β βββ Resources
β βββ components
β β βββ Navigation.tsx
β β βββ admin
β β βββ DecisionPanel.tsx
β β βββ ExtendedCurfewDetailModal.tsx
β β βββ PassRequestDetailModal.tsx
β β βββ WorkScheduleDetailModal.tsx
β β βββ providers
β β βββ SessionProvider.tsx
β βββ lib
β β βββ auth.ts
β β βββ prisma.ts
β βββ types
β βββ next-auth.d.ts
βββ .next
βββ app-build-manifest.json
βββ build-manifest.json
βββ cache
βββ server
βββ static
### Scripts
- `npm run dev` β Start the development server
- `npm run build` β Build for production
- `npm start` β Start the production server
- `npm run lint` β Run linter
---
## Database Management
- **Prisma Studio:**
```bash
npx prisma studio
- Migrations:
npx prisma migrate dev --name <migration_name>
This project uses NextAuth.js for authentication with two roles:
- Staff (Admin): Can post and manage announcements.
- Resident: Can view announcements and request curfew extensions.
The resident appointment flow is integrated with Salesforce so that scheduled appointments are created as Salesforce Event records and also mirrored in the local SQLite database for app-specific rules and UI rendering. In the current codebase, this Salesforce-backed flow is used by /my-events and /calendar-form. The separate /schedule page still embeds a Calendly widget for classes and is not part of the Salesforce event flow.
- Frontend pages expose a fixed list of selectable calendars backed by
NEXT_PUBLIC_SF_OWNER_1,NEXT_PUBLIC_SF_OWNER_2, andNEXT_PUBLIC_SF_OWNER_3. - The app checks Salesforce for existing
Eventrecords before offering time slots. - When a resident books an appointment, the app first reserves a local
Appointment, then creates a SalesforceEvent, then stores the returned Salesforce id inAppointment.salesforceEventId. - The app also maintains a local
ScheduledEventmirror so the resident-facing event list can show a caseworker label and render quickly from Prisma. - Salesforce authentication is server-to-server OAuth using the client credentials grant. It is not tied to the logged-in resident's Salesforce identity.
- A signed-in user opens
/my-eventsor/calendar-formand selects a caseworker calendar owner id from the frontend environment-configured list. - The page calls
GET /api/get-available-slots?date=YYYY-MM-DD&ownerId=<salesforce-owner-id>. - The route validates the date and owner id, queries Salesforce
Eventrecords for overlapping time on that owner's calendar, and returns available 30-minute slots inside business hours. - The user submits
POST /api/submit-eventwithtitle,startDate,endDate, andownerId. - The route revalidates the interval, rechecks Salesforce for conflicts, creates a local
Appointmentreservation, creates the SalesforceEvent, writes the returned Salesforce event id back to the appointment, and syncs theScheduledEventmirror. - The resident event list calls
GET /api/my-events, which returns appointments from the local database and refreshes any existingScheduledEventmirror rows. - Deleting an event calls
DELETE /api/my-events/[id], which attempts to delete the SalesforceEvent, then deletes the local mirror and local appointment row.
The shared Salesforce client in samaritan_inn/src/lib/salesforce.ts uses:
POST {SF_LOGIN_URL}/services/oauth2/tokengrant_type=client_credentialsclient_id={SF_CLIENT_ID}client_secret={SF_CLIENT_SECRET}
The returned bearer token is then used for REST calls against SF_INSTANCE_URL. Missing Salesforce environment variables throw server-side errors. Salesforce API failures are normalized into SalesforceError and, for the main scheduling routes, are returned to the client as 502 responses with a generic scheduling-service error message.
| Endpoint | Method | Auth required | What it does | Response |
|---|---|---|---|---|
/api/get-available-slots |
GET |
No resident auth required for basic lookup; extra per-user limits only apply when a user session is present | Queries Salesforce Event records for one owner and date, then returns open 30-minute slots |
[{ start, end, label }] or { error } |
/api/submit-event |
POST |
Yes | Validates a booking, reserves a local appointment, creates a Salesforce Event, and stores the Salesforce id locally |
{ success: true, id } or { error } |
/api/my-events |
GET |
Yes | Returns the signed-in user's locally stored appointments and refreshes local mirror rows | [{ id, title, startTime, endTime, caseWorker, salesforceId, createdAt }] or { error } |
/api/my-events/[id] |
DELETE |
Yes | Deletes the user's local appointment and attempts to delete the linked Salesforce Event |
{ success: true } or { error } |
/api/get-calendar |
GET |
No | Queries Salesforce Event records between startDate and endDate and maps them into a simplified array |
[{ id, title, start, end, description, location }] |
Query parameters:
| Name | Required | Notes |
|---|---|---|
date |
Yes | Must be YYYY-MM-DD and within the current booking window. |
ownerId |
Yes | Must resolve to a valid 15-18 character Salesforce id. If omitted, server code falls back to SF_OWNER_ID. |
Behavior:
- Calls Salesforce with a SOQL query against
Event:SELECT Id, Subject, StartDateTime, EndDateTime, Description, Location FROM Event WHERE StartDateTime <= <day-end> AND EndDateTime >= <day-start> AND OwnerId = '<ownerId>'
- Uses only overlapping event intervals to compute availability.
- Returns 30-minute slots between
9:00 AMand5:00 PMCentral Time. - If a user session exists, the route also checks local appointment caps before returning slots.
Common errors:
400whendateis missing, malformed, outside the one-month window, orownerIdis invalid.409when the signed-in user already has an active appointment on that Central Time date.429when the signed-in user already has 7 active appointments.502when Salesforce authentication or querying fails.
Request body:
| Field | Required | Notes |
|---|---|---|
title |
Yes | Trimmed server-side before use. |
startDate |
Yes | Parsed with new Date(...); must be a valid appointment start instant. |
endDate |
Yes | Parsed with new Date(...); must be after startDate. |
ownerId |
Yes | Must resolve to a valid Salesforce id or the request fails. |
description |
No | Supported by the API and forwarded to Salesforce if present. Current first-party forms do not send it. |
location |
No | Supported by the API and forwarded to Salesforce if present. Current first-party forms do not send it. |
Response shape:
{ "success": true, "id": "<salesforce-event-id>" }Behavior:
- Requires an authenticated application user via NextAuth JWT.
- Rechecks Salesforce availability immediately before creating the event to reduce race conditions.
- Creates a local
Appointmentfirst, then creates the SalesforceEvent, then updates the local appointment withsalesforceEventId. - Syncs a local
ScheduledEventmirror row with the human-readable caseworker label. - On failure after the local reservation is created, the route cleans up by deleting the local reservation, deleting the mirror row, and deleting the Salesforce event if one was already created.
Common errors:
400for missing required JSON fields, invalid JSON, invalid owner id, invalid date format, out-of-window bookings, invalid duration, off-boundary start times, cross-day events, same-day bookings made with less than 5 hours lead time, or events outside9:00 AMto5:00 PMCentral Time.409when the chosen interval overlaps a SalesforceEventor the user already has an active booking on that day.429when the user already has 7 active appointments.502when Salesforce token exchange, event creation, or cleanup fails.500for unexpected server errors.
Response items:
| Field | Notes |
|---|---|
id |
Local Prisma Appointment.id, not the Salesforce id. |
title |
Local appointment title. |
startTime / endTime |
Stored locally as DateTime. |
caseWorker |
Derived from the selected owner id using frontend environment-backed labels. |
salesforceId |
Copied from Appointment.salesforceEventId. |
createdAt |
Local creation timestamp. |
Notes:
- The route reads from Prisma, not directly from Salesforce.
- Before returning results, it refreshes the local
ScheduledEventmirror for appointments that already have a Salesforce id. - If an owner id no longer matches one of the configured
NEXT_PUBLIC_SF_OWNER_*values, the fallback label returned is the raw owner id string.
Behavior:
- Requires an authenticated application user.
- Verifies that the appointment belongs to the requesting user.
- Attempts to delete the Salesforce
EventifsalesforceEventIdexists. - Deletes the local
ScheduledEventmirror and localAppointmenteven if the Salesforce delete call fails.
Common errors:
401when the user is not signed in.404when the appointment does not exist or does not belong to the user.
Current code indicates this is a separate, older Salesforce route:
- It performs its own token exchange instead of reusing
src/lib/salesforce.ts. - It accepts
startDateandendDatequery parameters. - It queries Salesforce
Eventrecords in that date range and returns a simplified array. - It does not filter by
OwnerId. - No current UI references to this route were found in the repository.
- It logs the token response and Salesforce response to the server console and does not have explicit route-level error handling.
The current implementation only reads and writes Salesforce Event records.
| Salesforce field | Source in app |
|---|---|
Subject |
Appointment.title / request title |
StartDateTime |
Request startDate |
EndDateTime |
Request endDate |
Description |
Request description when present |
Location |
Request location when present |
OwnerId |
Selected calendar owner id |
WhatId |
Hardcoded to 001gK00000hBCOaQAO during event creation |
Notes:
- Current code indicates
WhatIdis fixed for every created event; the repository does not explain what Salesforce record that id belongs to. - The app stores the returned Salesforce event id locally as
Appointment.salesforceEventIdand mirrors it again asScheduledEvent.salesforceId. - Caseworker labels are not fetched from Salesforce. They are derived from locally configured owner ids and hardcoded labels such as
Case Worker 1.
| Local model | Purpose |
|---|---|
Appointment |
Source of truth inside the app for resident bookings, user ownership, booking caps, owner id, and the linked salesforceEventId |
ScheduledEvent |
Local display mirror keyed by appointmentId, with a resolved caseworker label and optional salesforceId |
The current schema also includes Appointment.canceledAt, but the Salesforce scheduling flow shown in this repository does not currently use that field.
- Booking time zone is fixed to
America/Chicago. - Only 30-minute and 60-minute appointments are allowed.
- Appointments must start on the hour or half hour.
- Appointments must start and end on the same Central Time calendar day.
- Appointments must stay within
9:00 AMto5:00 PMCentral Time. - Same-day appointments must start at least 5 hours in the future.
- The booking window is from today through one month ahead.
- A user may have at most 7 active appointments.
- A user may have only 1 active appointment per Central Time date.
- One-hour bookings depend on two consecutive 30-minute slots being available.
- There is no dedicated schema-validation library in this flow; validation is implemented manually in route handlers and helper functions.
| Variable | Required | Used for |
|---|---|---|
SF_LOGIN_URL |
Yes | OAuth token endpoint base URL |
SF_CLIENT_ID |
Yes | Salesforce client credentials grant |
SF_CLIENT_SECRET |
Yes | Salesforce client credentials grant |
SF_INSTANCE_URL |
Yes | Base URL for Salesforce REST API calls after token exchange |
SF_OWNER_ID |
Optional fallback | Default owner id if an API caller omits ownerId |
NEXT_PUBLIC_SF_OWNER_1 |
Yes for current UI | Frontend-selectable calendar owner id and label mapping |
NEXT_PUBLIC_SF_OWNER_2 |
Yes for current UI | Frontend-selectable calendar owner id and label mapping |
NEXT_PUBLIC_SF_OWNER_3 |
Yes for current UI | Frontend-selectable calendar owner id and label mapping |
NEXTAUTH_SECRET |
Yes | Required to authenticate application users before booking or deleting events |
The root README setup example already includes DATABASE_URL, NEXTAUTH_URL, and NEXTAUTH_SECRET. For Salesforce-backed scheduling to work locally, the Salesforce variables above must also be present.
- Local development requires valid Salesforce credentials and reachable Salesforce org URLs; otherwise Salesforce-backed routes fail.
- The repository includes mock user scripts at
samaritan_inn/prisma/add-mock-caseworkers.jsandsamaritan_inn/prisma/add-mock-residents.js, but these only seed local Prisma users. They do not seed Salesforce calendars, owners, or events. - No automated tests, fixtures, or seed data specific to Salesforce event creation were found in the current repository.
/my-eventsand/calendar-formboth implement the same Salesforce-backed scheduling UI flow./schedulestill uses a Calendly embed for classes, so the app currently contains both a Salesforce-backed appointment flow and a separate legacy Calendly flow./api/get-calendarappears to be older or auxiliary code and is not clearly integrated into the current UI.
- Build the application:
npm run build
- Start the production server:
npm start
- Allow admin to add attachements to announcements and events
- Change curfew to more of a form and then sends admin response to user in form of email or notification
- Retire the legacy Calendly scheduling page in favor of the Salesforce-backed scheduling flow already used by
/my-eventsand/calendar-form - User request form to fulfill order in inflow.