This is my NYS Parks & Recreation capstone project. It is a public-facing parks website with role-based account areas for clients, employees, and admins. The project is intentionally built as a flat PHP/MySQL application so it is easy to run in XAMPP, easy to review, and easy to submit without needing a framework install or complicated server routing.
The site combines public park discovery pages, event listings, news updates, a searchable FAQ, a mock donation flow, a map page, an AI guide page, and private dashboards for different user roles. Most pages keep the original static front-end structure, but the important workflows are connected to MySQL through bootstrap.php and prepared PDO queries.
This final package uses the polished p7r18 page structure and comments as the base, with the stable p7r19 register.php flow kept for client account registration.
The main goal of this build is to show a complete, understandable, database-backed parks portal. I focused on making the project practical for a class/capstone review instead of overcomplicating it with extra folders or framework files.
The project demonstrates:
- a professional public NYS Parks style website
- public pages that visitors can use without logging in
- private pages protected by role checks
- client reservation/event-request workflows
- employee schedule and PTO workflows
- admin approval, employee management, news management, and CSV export workflows
- reusable shared navigation, footer, Bootstrap styling, and JavaScript helpers
- a MySQL schema with foreign keys, indexes, enum fields, and check constraints
- seeded test data so the project can be demoed immediately after import
- PHP for server-side pages and form handling
- MySQL / MariaDB for the relational database
- PDO for database access
- Bootstrap 5.3.3 for layout, cards, forms, buttons, grid, and responsive design
- Bootstrap Icons for navigation and UI icons
- Vanilla JavaScript for filtering, FAQ search, news expansion, donation form toggles, AI chat UI, and chart setup
- Chart.js for dashboard charts where available
- XAMPP as the intended local runtime
- HTML/CSS with one shared stylesheet in
css/styles.css
There are no Laravel, Symfony, React, Node, Composer, or npm requirements.
The project is flat on purpose. All main pages live in the root folder.
p7r18_base_p7r19_register_final/
├── about.php
├── account.php
├── admin-bookings.php
├── admin-dashboard.php
├── admin-employee-accounts.php
├── admin-employee-schedule.php
├── admin-news.php
├── admin-pto.php
├── admin-csv.php
├── ai.php
├── ai-api.php
├── bootstrap.php
├── client-create-event.php
├── client-dashboard.php
├── donate.php
├── employee-dashboard.php
├── employee-pto.php
├── employee-schedule.php
├── events.php
├── faq.php
├── forgot-password.php
├── index.php
├── login.php
├── logout.php
├── map.php
├── map-api.php
├── news.php
├── parks.php
├── register.php
├── reset-password.php
├── search.php
├── css/
│ └── styles.css
├── includes/
│ ├── constants.php
│ ├── header.php
│ └── footer.php
├── js/
│ ├── app.js
│ └── dashboard-charts.js
├── db/
│ ├── schema.sql
│ └── seed.sql
├── README.md
└── todo - done - data - pgs - sql - etc .xlsx
The .idea/ folder is only IDE metadata. It is not required to run the project and should be removed from a clean final submission. The spreadsheet is a planning/checklist artifact, not a runtime dependency.
This is the shared backend setup file. Most dynamic PHP pages require it.
It handles:
- session startup
- secure-ish session cookie settings for local project use
- PDO database connection
- current user lookup
- login and logout helpers
- role protection helpers
- flash messages
- HTML escaping helper
e() - date formatting helpers
- input helpers for
POSTandGET - mock card validation
- CSV export filename helper
The database settings are currently inside the db() function:
$host = '127.0.0.1';
$port = '3306';
$dbname = 'nys_parks';
$username = 'root';
$password = '';These are the shared layout partials. They keep the public navigation, account/login links, footer columns, and common page shell consistent across the flat PHP pages.
This is the shared custom stylesheet. It contains the site shell, hero sections, cards, dashboards, filter panels, map layout, event/news tiles, footer, and responsive polish.
This is the shared JavaScript file for common front-end behavior.
It handles:
- active navigation highlighting based on
data-page - confirmation prompts for delete/review actions
- news search/filter behavior
- news “Open article / Close article” collapse behavior
- FAQ search/filter behavior
- donation payment method UI
- AI chat form UI and API call handling
This supports dashboard chart rendering and chart fallback behavior. It also has helper logic for reservation payment form toggles.
This creates the full nys_parks database from scratch.
This inserts demo records for parks, users, fields, events, news, bookings, attendance, payments, employee schedules, and PTO requests.
Copy the full project folder into XAMPP htdocs.
Example:
C:\xampp\htdocs\p7r18_base_p7r19_register_final
Open XAMPP Control Panel and start:
- Apache
- MySQL
Open phpMyAdmin:
http://localhost/phpmyadmin
Then import these files in this order:
db/schema.sqldb/seed.sql
The schema file already includes:
DROP DATABASE IF EXISTS nys_parks;
CREATE DATABASE nys_parks CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE nys_parks;So it will rebuild the database cleanly.
Open bootstrap.php and confirm these values match your local MySQL setup:
$host = '127.0.0.1';
$port = '3306';
$dbname = 'nys_parks';
$username = 'root';
$password = '';For a normal XAMPP install, root with a blank password should work.
Use:
http://localhost/p7r18_base_p7r19_register_final/index.php
The project can also be deployed on InfinityFree.com using the same general flow as the local XAMPP setup: upload the PHP files, create/import the MySQL database, update the database connection credentials, and open the hosted URL.
Live website URL:
[ADD LIVE URL HERE]Domain/subdomain used:
[ADD DOMAIN OR INFINITYFREE SUBDOMAIN HERE]Hosting provider: InfinityFree.com
Domain provider/registrar:
[ADD DOMAIN SOURCE HERE: InfinityFree free subdomain / custom domain registrar / other]
- Log in to InfinityFree.
- Create a new hosting account.
- Choose either a free InfinityFree subdomain or connect a custom domain.
- Wait for the hosting account and domain/subdomain to become active.
- Open the InfinityFree hosting control panel.
- Open File Manager.
- Go to the site
htdocsfolder. - Upload the project source files into
htdocs.
The uploaded files should include the root PHP pages plus these folders:
css/js/includes/db/
The .idea/ folder and planning spreadsheet are not required for live hosting.
- In the InfinityFree control panel, open the MySQL database tool and create a database.
- Note the database name, username, password, and MySQL hostname provided by InfinityFree.
- Open phpMyAdmin from the InfinityFree control panel.
- Import the SQL files in this order:
db/schema.sqldb/seed.sql
This creates the nys_parks tables and inserts demo data for parks, users, events, news, bookings, payments, schedules, and PTO requests.
For local XAMPP, bootstrap.php uses the default local credentials:
$host = '127.0.0.1';
$port = '3306';
$dbname = 'nys_parks';
$username = 'root';
$password = '';For InfinityFree, these values must be changed to the database credentials shown in the InfinityFree control panel. Use the hosting database name, username, password, and MySQL hostname instead of the local XAMPP values.
Example placeholder format:
$host = '[ADD INFINITYFREE MYSQL HOSTNAME]';
$port = '3306';
$dbname = '[ADD INFINITYFREE DATABASE NAME]';
$username = '[ADD INFINITYFREE DATABASE USERNAME]';
$password = '[ADD INFINITYFREE DATABASE PASSWORD]';Do not submit a public copy of the project with real database passwords exposed. Replace passwords with placeholders in documentation when submitting screenshots or written instructions.
After upload and database import, open the live URL and test these pages/workflows:
index.phphome pageparks.phpparks filteringevents.phpevent browsing and RSVP behaviornews.phpnews listingsmap.phpmap/park cardslogin.phplogin using seeded accounts- client dashboard and private event request
- employee schedule and PTO request
- admin dashboard, booking approval, news management, PTO approval, and CSV export
Use the seeded demo accounts from the Seeded login accounts section below. The default demo password is Password123!.
When submitting, paste the live website URL in the assignment comments section so the instructor can test it directly:
Live website URL: [ADD LIVE URL HERE]
The seeded password for all demo accounts is:
Password123!
Main demo accounts:
| Role | Purpose | |
|---|---|---|
| Admin | admin@nysparks.local |
Admin dashboard, employees, approvals, news manager, CSV exports |
| Employee | employee@nysparks.local |
Employee dashboard, schedule, PTO requests |
| Client | client@nysparks.local |
Client dashboard, RSVPs, event requests, donations |
DEMO ACCOUNT PASSWORDS: Password123!
Additional seeded employees:
| Name | Role | |
|---|---|---|
| Casey Morgan | casey.morgan@nysparks.local |
Employee |
| Taylor Brooks | taylor.brooks@nysparks.local |
Employee |
The project has two main sides: public visitor pages and private role-based pages.
These pages can be viewed without logging in:
| File | Page | What it does |
|---|---|---|
index.php |
Home | Landing page with hero, search entry, featured parks, featured events, and public CTAs |
parks.php |
Explore Parks | Search/filter parks by keyword, region, and type using records from parks |
events.php |
Events | Shows public and private events from events, with filtering and RSVP actions |
map.php |
Map | Shows park locations and cards using parks data and optional Google Maps support |
news.php |
News | Shows published public updates from the news table with topic/region/search filters |
about.php |
About Us | Static public information page about the project/site |
faq.php |
FAQ | Searchable/filterable FAQ stored in a PHP array for now |
donate.php |
Donate | Donation page with mock card/in-person payment handling |
ai.php |
AI Guide | Public AI assistant page that connects to ai-api.php |
search.php |
Search | Sitewide search across parks, events, and news |
login.php |
Login | User login page |
register.php |
Register | New client account registration |
forgot-password.php |
Forgot Password | Verifies a user by email/name for local reset flow |
reset-password.php |
Reset Password | Updates the password after verification |
logout.php |
Logout | Ends the session and returns the user to the public site |
Some public pages have optional logged-in behavior. For example, the events page can show RSVP actions when a client is logged in.
These pages require login and role checks through require_login() or require_role().
| File | Role | What it does |
|---|---|---|
account.php |
Any logged-in user | Shared profile/account page with role-specific stats and action links |
client-dashboard.php |
Client | Client overview, RSVP records, bookings, payments, and event request actions |
client-create-event.php |
Client | Allows a client to submit a private event/booking request |
employee-dashboard.php |
Employee | Employee home dashboard with upcoming schedule and PTO overview |
employee-schedule.php |
Employee | Read-only employee shift schedule |
employee-pto.php |
Employee | Submit/cancel PTO requests and view PTO history |
admin-dashboard.php |
Admin | Main admin overview with metrics, charts, and links to admin tools |
admin-employee-accounts.php |
Admin | Create, update, disable, and reactivate employee accounts |
admin-news.php |
Admin | Create, update, search, and delete news records |
admin-bookings.php |
Admin | Review, approve, deny, and link booking requests to private events |
admin-employee-schedule.php |
Admin | Create/delete employee shift schedules with overlap validation |
admin-pto.php |
Admin | Approve or deny employee PTO requests |
admin-csv.php |
Admin | Export database datasets as CSV downloads |
A visitor can start on the home page and move through the public content:
- Visit
index.php - Search for parks or browse featured sections
- Go to
parks.phpto filter parks - Go to
events.phpto browse programs and events - Go to
news.phpto read updates and public notices - Use
map.phpfor park location browsing - Use
faq.phpfor common questions - Register or log in if they want account actions
A client can:
- Register on
register.php - Log in through
login.php - View their dashboard on
client-dashboard.php - RSVP to public events from
events.php - Create a private event request through
client-create-event.php - Track booking status and payment status on the dashboard
- Donate through
donate.php - Update profile details in
account.php
An employee can:
- Log in through
login.php - View their dashboard on
employee-dashboard.php - Review their shift schedule on
employee-schedule.php - Submit PTO through
employee-pto.php - Cancel pending PTO requests if needed
- Update profile details in
account.php
An admin can:
- Log in through
login.php - View metrics on
admin-dashboard.php - Create/update/disable employee accounts on
admin-employee-accounts.php - Add/update/delete public news records on
admin-news.php - Approve or deny booking requests on
admin-bookings.php - Create or delete employee schedules on
admin-employee-schedule.php - Approve or deny PTO requests on
admin-pto.php - Export CSV datasets through
admin-csv.php
parks.php loads park records from the parks table and supports filtering by:
- keyword
- region
- park type
Park cards include public-facing information such as region, type, description, summary, image, and amenities.
events.php loads events from the events table and joins parks for park names/regions. It supports public event browsing and client RSVP behavior.
Important event behavior:
- events have timing states like upcoming, live, past, or cancelled
- event listings can use image fields from
eventsor fall back to park images - RSVP records are stored in
attendance - duplicate active RSVP records are prevented by the unique attendance constraint
- capacity is checked in PHP before a new RSVP is accepted
- RSVP cancellation updates attendance status instead of deleting the row
news.php loads records from the news table.
News records include:
- title
- topic
- published date
- region
- summary
- full content
- image URL and alt text
- tag
- status
The page supports:
- keyword search
- topic filter buttons
- region filter
- update count display
- open/close article expansion in the card
The public news cards are database-backed. Unlike the FAQ page, news content is not hardcoded in the PHP page.
faq.php currently uses a PHP array named $faqItems. It is not database-backed yet.
The page still has dynamic front-end behavior:
- search box
- topic filter buttons
- result count
- Bootstrap accordion items
This was kept simple so the FAQ can be edited directly in the page. A future version could move FAQs into a new database table without changing the public layout much.
donate.php supports a mock donation workflow.
Important details:
- donations are stored in
payments - payment type is
donation - the flow supports card and in-person/pledge style options
- card validation is for class/demo use only
- CVV and full card data are not production-safe and should not be used like this in a real site
- card expiration is checked in PHP with
validate_card()
client-create-event.php allows clients to request private events.
The request is stored in bookings with a pending status.
The page validates:
- required fields
- selected park/field
- date/time ordering
- guest count
- field capacity
- overlapping active bookings for the same field/time range
Payment is not collected on client-create-event.php. The booking request stores a calculated reservation fee, and the client dashboard displays booking/payment status for review/demo purposes.
admin-bookings.php lets admins approve or deny bookings.
When a booking is approved, the code creates or updates a linked private event in the events table and stores that event ID in the booking record.
This is the intended flow:
- Client submits a booking request.
- A
bookingsrow is created withbooking_status = 'pending'. - Admin reviews it.
- If approved, a private
eventsrow is created or updated. - The booking gets
event_id,reviewed_by,reviewed_at, and admin notes.
admin-employee-schedule.php lets admins create employee shifts.
The code checks for overlapping shifts for the same employee/date before inserting a new schedule.
Employees view schedules on:
employee-dashboard.phpemployee-schedule.php
Employees cannot edit their own schedule from those pages.
Employees submit PTO through employee-pto.php.
The code checks:
- start date is not after end date
- required fields are present
- overlapping pending/approved PTO requests do not already exist
Admins approve/deny PTO in admin-pto.php.
Approval metadata is stored in:
reviewed_byreviewed_atadmin_notes
admin-dashboard.php includes query-driven counts and dashboard sections.
It uses database queries for:
- park count
- event count
- approved bookings
- pending bookings
- pending PTO
- attendance counts
- upcoming RSVP guest totals
- active employee counts
- chart data for bookings, events, attendance, and donations
Employee account management and news management now live on dedicated admin pages: admin-employee-accounts.php and admin-news.php. The dashboard links to those tools instead of handling those mutations directly.
admin-csv.php exports selected datasets as CSV downloads.
It does not require filesystem write permissions because it sends the CSV directly as a download response.
Supported export-style datasets include:
- parks
- users
- employees
- fields
- events
- news
- bookings
- attendance
- payments
- employee schedules
- PTO requests
ai.php provides the public AI guide interface.
ai-api.php handles the backend API call pattern. The front-end sends the user message and recent chat history. If a live API key/backend is not configured, the page still provides a clear place for that feature.
map.php loads park records and prepares map-friendly data such as name, region, address, latitude, longitude, image, and summary.
map-api.php contains a helper for checking whether a Google Maps API key is configured.
The current schema uses 10 tables:
parksusersfieldseventsnewsbookingsattendancepaymentsemployee_schedulespto_requests
The older README said the database had 9 tables and did not include news. That is no longer accurate. The current project includes a full news table and the News page reads from it.
db/seed.sql includes sample data for demo/testing.
Approximate seeded content:
| Table | Seed data included |
|---|---|
parks |
12 parks |
users |
5 users |
fields |
6 facilities/fields |
events |
24 events |
news |
6 public news updates |
bookings |
4 booking requests |
attendance |
2 attendance/RSVP records |
payments |
2 payments |
employee_schedules |
4 schedule records |
pto_requests |
4 PTO requests |
The seed data is designed so the dashboards and public pages have something to show right away.
The full executable SQL is in db/schema.sql. This section explains what each table does and lists the most important keys, constraints, and indexes.
Stores public park/location records.
Important fields:
idnameregionpark_type- address fields
hourstotal_fieldsmax_capacityamenitiesdescriptionimage_urlimage_altcard_summarylatitudelongitudeis_featured- timestamps
Important constraints/indexes:
PRIMARY KEY (id)
UNIQUE (name)
CHECK (total_fields >= 0)
CHECK (max_capacity > 0)
INDEX idx_parks_region (region)
INDEX idx_parks_type (park_type)
INDEX idx_parks_featured (is_featured)Stores all users: clients, employees, and admins.
Important fields:
idfirst_namelast_nameemailpassword_hashrolephonebirthdateorganizationnotespark_idaccount_statuslast_login_at
Role values:
ENUM('client','employee','admin')Status values:
ENUM('active','locked','disabled')Important constraints/indexes:
PRIMARY KEY (id)
UNIQUE (email)
FOREIGN KEY (park_id) REFERENCES parks(id) ON DELETE SET NULL
INDEX idx_users_role_status (role, account_status)
INDEX idx_users_email_status (email, account_status)
INDEX idx_users_park (park_id)Stores reservable fields/facilities that belong to parks.
Important fields:
idpark_idnamefield_typecapacityfield_size_sqftavailability_statusnotes
Availability values:
ENUM('available','unavailable','maintenance')Important constraints/indexes:
FOREIGN KEY (park_id) REFERENCES parks(id) ON DELETE CASCADE
CHECK (capacity > 0)
CHECK (field_size_sqft IS NULL OR field_size_sqft > 0)
UNIQUE KEY uq_field_name_per_park (park_id, name)
INDEX idx_fields_park_status (park_id, availability_status)Stores both public events and private events created from approved bookings.
Important fields:
idpark_idfield_idtitledescriptionimage_urlimage_altcard_summarycategoryevent_typestart_datetimeend_datetimecapacityfee_amountis_featuredevent_statuscreated_by
Event type values:
ENUM('public','private')Event status values:
ENUM('draft','published','closed','cancelled','completed')Important constraints/indexes:
FOREIGN KEY (park_id) REFERENCES parks(id) ON DELETE RESTRICT
FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE SET NULL
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
CHECK (start_datetime < end_datetime)
CHECK (capacity > 0)
CHECK (fee_amount >= 0)
INDEX idx_events_status_dates (event_status, start_datetime)
INDEX idx_events_park_dates (park_id, start_datetime)
INDEX idx_events_type_status_dates (event_type, event_status, start_datetime)
INDEX idx_events_category (category)
INDEX idx_events_featured (is_featured)Stores public news feed items used by news.php and managed from admin-news.php.
Important fields:
idtitletopicpublished_dateregionsummarycontentimage_urlimage_altcard_summarytagis_featurednews_status
Topic values:
ENUM('alerts','community','events','parks','safety','support','conservation','volunteer','maintenance','education','seasonal')Status values:
ENUM('draft','published','archived')Important indexes:
INDEX idx_news_status_date (news_status, published_date)
INDEX idx_news_topic_date (topic, published_date)
INDEX idx_news_region (region)
INDEX idx_news_featured (is_featured)Stores client private event/field reservation requests.
Important fields:
idclient_idpark_idfield_idevent_idtitlebooking_typeattendee_emailstart_datetimeend_datetimeguest_countrequested_setupevent_descriptionspecial_requestsreservation_feebooking_status- review metadata
Booking status values:
ENUM('pending','approved','denied','cancelled','confirmed','completed')Important constraints/indexes:
FOREIGN KEY (client_id) REFERENCES users(id) ON DELETE CASCADE
FOREIGN KEY (park_id) REFERENCES parks(id) ON DELETE RESTRICT
FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE SET NULL
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE SET NULL
FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL
CHECK (start_datetime < end_datetime)
CHECK (guest_count > 0)
CHECK (reservation_fee >= 0)
UNIQUE KEY uq_bookings_event (event_id)
INDEX idx_bookings_status_dates (booking_status, start_datetime)
INDEX idx_bookings_client_status (client_id, booking_status)
INDEX idx_bookings_park_dates (park_id, start_datetime)
INDEX idx_bookings_reviewed_by (reviewed_by)Stores event RSVP/attendance records.
Important fields:
idevent_iduser_idattendee_emailguest_countattendance_statusregistered_atchecked_in_at
Attendance status values:
ENUM('registered','cancelled','attended','no_show')Important constraints/indexes:
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
CHECK (guest_count > 0)
UNIQUE KEY uq_attendance_event_email (event_id, attendee_email)
INDEX idx_attendance_event_status (event_id, attendance_status)
INDEX idx_attendance_user (user_id)
INDEX idx_attendance_email (attendee_email)Stores mock reservation and donation payments.
Important fields:
iduser_idbooking_idpayment_typepayer_namepayer_emailamountpayment_methodcard_numexp_monthexp_yearcvvpayment_statustransaction_ref
Payment type values:
ENUM('reservation','donation')Payment method values:
ENUM('card','in_person')Payment status values:
ENUM('pending','completed','failed','refunded')Important constraints/indexes:
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL
CHECK (amount > 0)
CHECK (payment_method <> 'card' OR (card_num IS NOT NULL AND card_num REGEXP '^[0-9]{13,19}$'))
CHECK (payment_method <> 'card' OR (exp_month IS NOT NULL AND exp_month BETWEEN 1 AND 12))
CHECK (payment_method <> 'card' OR (exp_year IS NOT NULL AND exp_year BETWEEN 2026 AND 2100))
CHECK (payment_method <> 'card' OR (cvv IS NOT NULL AND cvv REGEXP '^[0-9]{3,4}$'))
UNIQUE (transaction_ref)
INDEX idx_payments_type_status (payment_type, payment_status)
INDEX idx_payments_user (user_id)
INDEX idx_payments_booking (booking_id)
INDEX idx_payments_date (created_at)Project note: this is mock payment storage for a class project. It is not production payment handling.
Stores employee shifts.
Important fields:
idemployee_idpark_idshift_datestart_timeend_timeassignmentschedule_statusnotescreated_by
Schedule status values:
ENUM('scheduled','cancelled','completed')Important constraints/indexes:
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
FOREIGN KEY (park_id) REFERENCES parks(id) ON DELETE RESTRICT
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
CHECK (start_time < end_time)
INDEX idx_sched_employee_date (employee_id, shift_date)
INDEX idx_sched_park_date (park_id, shift_date)
INDEX idx_sched_status (schedule_status)Stores employee time-off requests.
Important fields:
idemployee_idleave_typestart_dateend_datereasonpto_statusreviewed_byreviewed_atadmin_notes
PTO status values:
ENUM('pending','approved','denied','cancelled')Important constraints/indexes:
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL
CHECK (start_date <= end_date)
INDEX idx_pto_employee_status (employee_id, pto_status)
INDEX idx_pto_dates_status (start_date, end_date, pto_status)
INDEX idx_pto_reviewed_by (reviewed_by)Some rules are intentionally handled in PHP instead of triggers.
The schema comments call out these dynamic checks:
- card expiration must not be before the current month/year
- event capacity should not exceed selected field capacity
- booking guest count should not exceed selected field capacity
- active bookings should not overlap for the same field and time range
- attendance guest totals should not exceed event capacity
I kept these in PHP because they depend on current date/time or workflow context and are easier to explain/test in a simple XAMPP project.
Authentication is session-based.
Important helper functions:
login_user($user)logout_user()current_user($db)require_login($db)require_role($db, $roles)
Private pages use role checks at the top of the file. For example:
$user = require_role($db, 'admin');or:
$user = require_role($db, 'client');The shared account page uses:
$user = require_login($db);If a user is inactive, disabled, or not logged in, the helper functions redirect them appropriately.
This is a student/capstone project, but it still includes several safe patterns:
- uses PDO prepared statements for user input in database queries
- escapes output with
e()before printing dynamic values - hashes passwords with
password_hash() - verifies passwords with
password_verify() - uses role-based access checks for private pages
- prevents private page caching with headers in protected flows
- uses flash messages for status/error feedback
Important limitations:
- there is no CSRF token system yet
- payment handling is mock/demo only
- CVV/card fields should not be stored in a real production system
- password reset does not send email or use expiring reset tokens
- API keys should be loaded from environment variables, not committed to source
- AI API configuration depends on local setup/environment
- database credentials are currently local XAMPP defaults in
bootstrap.php
This project does not send password reset emails.
The local reset flow is:
- User opens
forgot-password.php - User enters email, first name, and last name
- The page checks for an active matching user in
users - If matched, the user ID is temporarily stored in session
- User is redirected to
reset-password.php - New password is saved with
password_hash() - The reset session value is cleared
- User logs in normally
This is simpler than token/email reset and works for a local class demo.
Payments are intentionally mock-only.
The current payment design supports:
- reservation payments
- donations
- card method
- in-person method
- transaction reference values
- payment statuses
The schema has card checks for number length, expiration month/year, and CVV format. The PHP also checks that the expiration is not in the past.
For a real production site, this would need a payment processor like Stripe, Square, or another PCI-compliant provider. This project should not be used to store real card numbers.
The CSV export page is admin-only.
The export is generated directly in PHP and downloaded immediately. This avoids folder permission problems because the server does not need to write CSV files to disk.
This is useful for the project because it demonstrates structured data output without adding storage folders or export log tables.
Not every page uses database tables.
faq.phpstores FAQ content in a PHP array.about.phpis mainly static content.- Shared headers and footers are now included through
includes/header.phpandincludes/footer.php.
The FAQ/static pages were kept simple so the project remains easy to follow in a flat PHP submission.
This section is kept from the original README and updated to match the current project.
- Root-level PHP pages that keep the original static page structure as closely as possible
- Original class names, Bootstrap layout, navigation, footer, and shared stylesheet/script retained
- Dynamic PHP/MySQL wiring added where needed
- Shared account/profile page unified for all roles
- Password reset simplified to a verify-user then reset flow using the
userstable and session state - Dashboard metrics are query-driven
- Booking/PTO approvals write review metadata
- Booking and schedule overlap validation added
- Payments kept as mock / card-validation-only / in-person support
- CSV export works as a direct download and does not require file-system write permissions
- News page now uses the
newsdatabase table - Dedicated admin pages handle employee account management and news create/update/delete management
- Database currently uses 10 tables
- FAQ remains a PHP array and can be moved to a database later
Before submitting or demoing, I should click-test these flows in XAMPP:
- Home page loads
- Parks page loads and filters parks
- Events page loads and filters events
- News page loads published news and article buttons open/close
- FAQ page search and topic filter works
- Map page loads park cards/map content
- Donate page loads
- Search page returns parks/events/news results
- Register new client
- Login
- Logout
- Forgot password to reset password flow
- Account/profile update
- Client dashboard loads
- RSVP to a public event
- Cancel RSVP
- Submit private event request
- View booking status
- Submit donation as logged-in client
- Employee dashboard loads
- Employee schedule loads
- Submit PTO request
- Cancel pending PTO request
- Admin dashboard loads
- Create employee
- Update employee
- Disable employee
- Create news item
- Update news item
- Delete news item
- Approve booking
- Deny booking
- Confirm approved booking creates/updates private event
- Create employee schedule
- Confirm schedule overlap prevention
- Delete schedule
- Approve PTO
- Deny PTO
- Download CSV export
Check:
- Apache is running
- MySQL is running
- database imported successfully
- database name is
nys_parks - credentials in
bootstrap.phpmatch your MySQL setup
Check that db/seed.sql was imported after db/schema.sql.
Use:
admin@nysparks.local
Password123!
Check that the news table exists and has published rows:
SELECT * FROM news WHERE news_status = 'published';FAQ is hardcoded in faq.php, not in the database. Edit the $faqItems array directly.
The project has map fallback/helper behavior. If a Google Maps API key is required, configure that separately. The park data itself still comes from the parks table.
dashboard-charts.js depends on Chart.js being loaded. If Chart.js cannot load from CDN, the page shows fallback text.
These are realistic improvements that could be added after the capstone version:
- move FAQ content into a
faqsdatabase table - add CSRF tokens to all POST forms
- add stronger server-side validation messages by field
- connect donation/reservation payment to a real payment processor
- replace local password reset with email/token-based reset
- add admin CRUD for parks and events
- add employee schedule editing instead of create/delete only
- add attendance check-in tools for employees/admins
- add API key configuration through environment variables
- add pagination for news, events, and admin tables
- add image upload support instead of external image URLs
This build is intentionally optimized for:
- simple XAMPP setup
- readable PHP files
- clear SQL structure
- database-backed capstone workflows
- public/private page separation
- easy demo accounts
- easier grading and review
The project is not meant to be a production NYS Parks system. It is a polished class/capstone version that shows the main site flow, database design, and role-based workflows in a way that can be run locally.