710 lines
27 KiB
PHP
710 lines
27 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
session_start();
|
||
}
|
||
|
||
require_once __DIR__ . '/../db/config.php';
|
||
|
||
function app_env(string $key, string $fallback = ''): string
|
||
{
|
||
$serverValue = $_SERVER[$key] ?? null;
|
||
if (is_string($serverValue) && trim($serverValue) !== '') {
|
||
return trim($serverValue);
|
||
}
|
||
|
||
$envValue = getenv($key);
|
||
if (is_string($envValue) && trim($envValue) !== '') {
|
||
return trim($envValue);
|
||
}
|
||
|
||
return $fallback;
|
||
}
|
||
|
||
function h(mixed $value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function text_length(string $value): int
|
||
{
|
||
return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
|
||
}
|
||
|
||
function site_name(): string
|
||
{
|
||
return app_env('PROJECT_NAME', 'Genitri Ecco Wellness');
|
||
}
|
||
|
||
function site_tagline(): string
|
||
{
|
||
return 'Nature-Based Experiential Tourism & Eco Wellness Services';
|
||
}
|
||
|
||
function default_meta_description(): string
|
||
{
|
||
return app_env(
|
||
'PROJECT_DESCRIPTION',
|
||
'Integrated nature-based experience ecosystem for eco wellness, experiential tourism, hospitality collaboration, and strategic partnership.'
|
||
);
|
||
}
|
||
|
||
function page_url(string $path, array $query = []): string
|
||
{
|
||
return $query ? $path . '?' . http_build_query($query) : $path;
|
||
}
|
||
|
||
function navigation_items(): array
|
||
{
|
||
return [
|
||
['key' => 'home', 'label' => 'Home', 'href' => 'index.php'],
|
||
['key' => 'about', 'label' => 'About Us', 'href' => 'about.php'],
|
||
['key' => 'services', 'label' => 'Services', 'href' => 'services.php'],
|
||
['key' => 'portfolio', 'label' => 'Portfolio', 'href' => 'portfolio.php'],
|
||
['key' => 'partnership', 'label' => 'Partnership', 'href' => 'partnership.php'],
|
||
['key' => 'publications', 'label' => 'Publications', 'href' => 'publications.php'],
|
||
['key' => 'contact', 'label' => 'Contact Us', 'href' => 'contact.php'],
|
||
];
|
||
}
|
||
|
||
function default_service_catalog(): array
|
||
{
|
||
return [
|
||
[
|
||
'slug' => 'eco-wellness',
|
||
'track' => 'immersive',
|
||
'category' => 'Eco Wellness Package',
|
||
'headline' => 'Preventive wellbeing journeys grounded in place.',
|
||
'summary' => 'Curated wellness experiences that combine guided recovery, quiet hospitality, and structured facilitation for premium groups.',
|
||
'audience' => 'Clinics, communities, executive wellness groups',
|
||
'duration' => 'Half day to 3 days',
|
||
'outcome' => 'Participants leave with a calmer baseline, curated routines, and professionally coordinated aftercare recommendations.',
|
||
'includes' => [
|
||
'Guided breathing, mobility, and reflection sessions',
|
||
'Nature immersion flow with on-site facilitator',
|
||
'Healthy hospitality setup and light nourishment',
|
||
'Operational coordinator, safety briefing, and guest support',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'retreat',
|
||
'track' => 'immersive',
|
||
'category' => 'Retreat Package',
|
||
'headline' => 'Immersive retreats with quiet luxury and operational discipline.',
|
||
'summary' => 'Multi-day retreat programs for restorative groups, leadership circles, or curated community cohorts seeking a trusted delivery partner.',
|
||
'audience' => 'Leadership teams, private communities, destination hosts',
|
||
'duration' => '2 to 4 days',
|
||
'outcome' => 'Guests receive a coherent retreat arc, documented experience moments, and a host-ready operating structure.',
|
||
'includes' => [
|
||
'Retreat flow design from arrival to closing circle',
|
||
'Hospitality curation and accommodation coordination',
|
||
'Experience documentation and facilitator scheduling',
|
||
'Transportation, guest movement, and contingency planning',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'corporate',
|
||
'track' => 'corporate',
|
||
'category' => 'Corporate Package',
|
||
'headline' => 'Nature-based programs for teams that need focus, recovery, and alignment.',
|
||
'summary' => 'Corporate offsites and culture journeys that balance executive standards, wellness objectives, and measurable group outcomes.',
|
||
'audience' => 'Companies, HR teams, executive assistants, corporate communities',
|
||
'duration' => '1 to 2 days',
|
||
'outcome' => 'A more connected team, clean guest logistics, and a premium experience that avoids generic event energy.',
|
||
'includes' => [
|
||
'Team reset sessions and guided collaboration moments',
|
||
'Hospitality coordination with venue and catering partners',
|
||
'Brand-safe itineraries, agendas, and support crew',
|
||
'Post-program recap for internal reporting',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'hospitality',
|
||
'track' => 'operational',
|
||
'category' => 'Hospitality Package',
|
||
'headline' => 'Hospitality collaborations that expand destination value.',
|
||
'summary' => 'Program overlays for properties, destinations, or operators wanting to add curated wellness and experiential layers to their guest offering.',
|
||
'audience' => 'Hotels, villas, destination operators, stay partners',
|
||
'duration' => 'Modular activation',
|
||
'outcome' => 'Partners gain a credible package layer, documented service standards, and a stronger experiential positioning.',
|
||
'includes' => [
|
||
'Experience packaging and guest-ready positioning',
|
||
'Venue flow, staffing, and hospitality service mapping',
|
||
'Wellness or retreat modules fitted to the property',
|
||
'Partner coordination with clear operating notes',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'event-mice',
|
||
'track' => 'corporate',
|
||
'category' => 'Event & MICE Package',
|
||
'headline' => 'Operationally credible events with experiential depth.',
|
||
'summary' => 'MICE and event support for organizations that need premium guest handling, destination texture, and dependable execution.',
|
||
'audience' => 'Associations, brands, institutions, MICE organizers',
|
||
'duration' => 'Single activation to multi-day agenda',
|
||
'outcome' => 'An event environment that feels curated, calm, and professionally managed from transport to closing session.',
|
||
'includes' => [
|
||
'Run-of-show design and ecosystem partner alignment',
|
||
'Guest logistics, transport, and venue operations support',
|
||
'Curated destination layers for delegates or VIP groups',
|
||
'Documentation for post-event portfolio use',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'custom-experience',
|
||
'track' => 'operational',
|
||
'category' => 'Custom Experience Package',
|
||
'headline' => 'Flexible program architecture for bespoke briefs.',
|
||
'summary' => 'A consultative route for institutions or partners who need tailored combinations of wellness, hospitality, logistics, and activation.',
|
||
'audience' => 'Strategic partners, institutions, multi-stakeholder projects',
|
||
'duration' => 'Scoped during consultation',
|
||
'outcome' => 'A custom proposal with the right operating modules, partner map, and delivery assumptions for the brief.',
|
||
'includes' => [
|
||
'Discovery call and brief clarification',
|
||
'Package architecture and ecosystem matchmaking',
|
||
'Operational assumptions for location, scale, and timing',
|
||
'Proposal-ready summary to support internal decision-making',
|
||
],
|
||
],
|
||
];
|
||
}
|
||
|
||
function service_by_slug(string $slug): ?array
|
||
{
|
||
foreach (service_catalog() as $service) {
|
||
if ($service['slug'] === $slug) {
|
||
return $service;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function default_portfolio_catalog(): array
|
||
{
|
||
return [
|
||
[
|
||
'category' => 'Retreat Documentation',
|
||
'title' => 'Immersive reset for healthcare leaders',
|
||
'summary' => 'Three-day private retreat with guided wellness, quiet hospitality, and destination movement planning.',
|
||
'detail' => 'Built around trusted facilitation, transport sequencing, curated dining, and premium guest care.',
|
||
'metric' => '48 delegates · 3 days · full service coordination',
|
||
],
|
||
[
|
||
'category' => 'Hospitality Activities',
|
||
'title' => 'Property-based eco wellness activation',
|
||
'summary' => 'Hospitality collaboration that layered preventive wellness modules into a destination stay offer.',
|
||
'detail' => 'Designed to expand partner value without disrupting existing property operations.',
|
||
'metric' => '6 touchpoints · 2 host teams · reusable package model',
|
||
],
|
||
[
|
||
'category' => 'Event Showcase',
|
||
'title' => 'Institutional offsite with nature-led programming',
|
||
'summary' => 'Corporate gathering that fused structured sessions, outdoor recovery windows, and logistics support.',
|
||
'detail' => 'Focused on calm execution, executive-safe service, and documented experience moments.',
|
||
'metric' => '120 participants · transport + venue + documentation',
|
||
],
|
||
[
|
||
'category' => 'Operational Support',
|
||
'title' => 'Partner coordination for a multi-vendor destination brief',
|
||
'summary' => 'Cross-functional partner map covering venue, mobility, hospitality, and merchandise support.',
|
||
'detail' => 'Organized into clear workstreams to reduce friction and improve delivery certainty.',
|
||
'metric' => '9 partner lanes · single briefing track · review-ready notes',
|
||
],
|
||
];
|
||
}
|
||
|
||
function default_partnership_categories(): array
|
||
{
|
||
return [
|
||
['title' => 'Medical Partner', 'summary' => 'Preventive health programs, professional credibility, and trusted wellness positioning.'],
|
||
['title' => 'Food & Beverage Partner', 'summary' => 'Healthy dining curation, premium service alignment, and guest experience enhancement.'],
|
||
['title' => 'Hospitality Partner', 'summary' => 'Hotels, villas, and destination stays that want elevated experiential packages.'],
|
||
['title' => 'Transportation Partner', 'summary' => 'Ground movement, transfer planning, and reliable guest logistics.'],
|
||
['title' => 'Vendor Partner', 'summary' => 'Operational vendors for events, staging, equipment, or curated supporting services.'],
|
||
['title' => 'Venue Partner', 'summary' => 'Destination spaces that fit wellness, retreat, and premium gathering formats.'],
|
||
['title' => 'Merchandise Partner', 'summary' => 'Thoughtful branded items, gifting, and supporting program materials.'],
|
||
['title' => 'Talent Partner', 'summary' => 'Facilitators, wellness practitioners, speakers, and curated experience talent.'],
|
||
['title' => 'Facilities Partner', 'summary' => 'Technical, utility, and infrastructure support for safe, smooth delivery.'],
|
||
];
|
||
}
|
||
|
||
function default_publication_catalog(): array
|
||
{
|
||
return [
|
||
[
|
||
'slug' => 'governance-wellness',
|
||
'category' => 'Whitepaper',
|
||
'date' => '2026-05-12',
|
||
'title' => 'Governance as a differentiator in eco wellness operations',
|
||
'summary' => 'A concise look at why ethical operating systems increase trust in nature-based programs.',
|
||
'body' => [
|
||
'Nature-based experiences become institution-ready when governance is visible, practical, and embedded into the delivery model.',
|
||
'For GBP, governance is not an afterthought. It shapes partner qualification, guest flow, documentation, safety considerations, and post-program reporting.',
|
||
'Publishing this kind of operational insight helps the website function as both a service platform and a credibility engine for future collaborations.',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'hospitality-ecosystem',
|
||
'category' => 'Strategic Insight',
|
||
'date' => '2026-04-28',
|
||
'title' => 'Why hospitality ecosystems outperform isolated program offers',
|
||
'summary' => 'Integrated destinations create stronger guest journeys when transport, venue, and wellness layers are coordinated early.',
|
||
'body' => [
|
||
'Standalone experiences can look attractive in a brochure but collapse under operational pressure when the surrounding ecosystem is weak.',
|
||
'An integrated hospitality ecosystem aligns venue readiness, movement planning, service staffing, food design, and escalation paths before guests arrive.',
|
||
'That alignment is what allows a premium minimalist brand like Genitri Ecco Wellness to stay calm, polished, and credible at scale.',
|
||
],
|
||
],
|
||
[
|
||
'slug' => 'experience-customization',
|
||
'category' => 'Ecosystem Update',
|
||
'date' => '2026-03-30',
|
||
'title' => 'A practical framework for customizing destination experiences',
|
||
'summary' => 'How inquiry intake, partner matching, and brief translation can stay lightweight while still feeling premium.',
|
||
'body' => [
|
||
'Customization is most effective when the intake process captures the essentials: audience, timing, desired outcome, and operating constraints.',
|
||
'From there, GBP can match the right service modules, partner types, and delivery assumptions without overcomplicating the first conversation.',
|
||
'This is why the first MVP slice focuses on a strategic inquiry desk that feels both editorial and operational.',
|
||
],
|
||
],
|
||
];
|
||
}
|
||
|
||
function publication_by_slug(string $slug): ?array
|
||
{
|
||
foreach (publication_catalog() as $publication) {
|
||
if ($publication['slug'] === $slug) {
|
||
return $publication;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function governance_values(): array
|
||
{
|
||
return [
|
||
['title' => 'Excellence with Integrity', 'summary' => 'Premium execution grounded in transparent standards, safety, and reliable delivery.'],
|
||
['title' => 'Shared Value Creation', 'summary' => 'Programs and partnerships designed to benefit guests, collaborators, and the wider ecosystem.'],
|
||
['title' => 'Sustainable Equilibrium', 'summary' => 'Nature, hospitality, and operations balanced with long-term resilience in mind.'],
|
||
];
|
||
}
|
||
|
||
function objective_list(): array
|
||
{
|
||
return [
|
||
'Build institutional trust',
|
||
'Showcase operational capability',
|
||
'Open strategic partnership routes',
|
||
'Generate qualified inquiry',
|
||
'Provide curated package services',
|
||
'Enable experience customization',
|
||
'Display ecosystem portfolio',
|
||
'Build publication authority',
|
||
'Prepare governance infrastructure',
|
||
];
|
||
}
|
||
|
||
function inquiry_type_options(): array
|
||
{
|
||
return [
|
||
'Strategic Partnership',
|
||
'Package Customization',
|
||
'Operational Collaboration',
|
||
'Publication / Media',
|
||
];
|
||
}
|
||
|
||
function interest_groups(): array
|
||
{
|
||
$services = [];
|
||
foreach (service_catalog() as $service) {
|
||
$services[] = $service['category'];
|
||
}
|
||
|
||
$partners = [];
|
||
foreach (partnership_categories() as $partner) {
|
||
$partners[] = $partner['title'];
|
||
}
|
||
|
||
return [
|
||
'Curated Programs' => $services,
|
||
'Partnership Routes' => $partners,
|
||
];
|
||
}
|
||
|
||
function scale_options(): array
|
||
{
|
||
return [
|
||
'Private group (10–30)',
|
||
'Team offsite (31–80)',
|
||
'Corporate / MICE (81–200)',
|
||
'Multi-site activation (200+)',
|
||
];
|
||
}
|
||
|
||
function location_options(): array
|
||
{
|
||
return [
|
||
'Tegal / Bregas corridor',
|
||
'Central Java destination',
|
||
'Partner property or venue',
|
||
'To be discussed during scoping',
|
||
];
|
||
}
|
||
|
||
function active_class(string $activePage, string $key): string
|
||
{
|
||
return $activePage === $key ? 'active' : '';
|
||
}
|
||
|
||
function current_year(): string
|
||
{
|
||
return gmdate('Y');
|
||
}
|
||
|
||
function csrf_token(): string
|
||
{
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
|
||
}
|
||
|
||
return $_SESSION['csrf_token'];
|
||
}
|
||
|
||
function verify_csrf_token(?string $token): bool
|
||
{
|
||
return is_string($token) && hash_equals($_SESSION['csrf_token'] ?? '', $token);
|
||
}
|
||
|
||
function flash_set(string $type, string $message): void
|
||
{
|
||
$_SESSION['flash'] = [
|
||
'type' => $type,
|
||
'message' => $message,
|
||
];
|
||
}
|
||
|
||
function flash_get(): ?array
|
||
{
|
||
if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
|
||
return null;
|
||
}
|
||
|
||
$flash = $_SESSION['flash'];
|
||
unset($_SESSION['flash']);
|
||
|
||
return $flash;
|
||
}
|
||
|
||
function old_value(array $source, string $key, string $fallback = ''): string
|
||
{
|
||
$value = $source[$key] ?? $fallback;
|
||
return trim((string) $value);
|
||
}
|
||
|
||
function is_selected(?string $candidate, ?string $value): string
|
||
{
|
||
return ((string) $candidate === (string) $value) ? 'selected' : '';
|
||
}
|
||
|
||
function app_db_connection(): ?PDO
|
||
{
|
||
static $attempted = false;
|
||
static $pdo = null;
|
||
static $error = null;
|
||
|
||
if ($attempted) {
|
||
return $pdo;
|
||
}
|
||
|
||
$attempted = true;
|
||
|
||
try {
|
||
$pdo = db();
|
||
$error = null;
|
||
} catch (Throwable $exception) {
|
||
$pdo = null;
|
||
$error = $exception->getMessage();
|
||
}
|
||
|
||
$GLOBALS['APP_DB_ERROR'] = $error;
|
||
|
||
return $pdo;
|
||
}
|
||
|
||
function app_db_error(): ?string
|
||
{
|
||
app_db_connection();
|
||
$error = $GLOBALS['APP_DB_ERROR'] ?? null;
|
||
return is_string($error) && $error !== '' ? $error : null;
|
||
}
|
||
|
||
function using_preview_storage(): bool
|
||
{
|
||
return app_db_connection() === null;
|
||
}
|
||
|
||
function inquiry_table_sql(): string
|
||
{
|
||
return <<<'SQL'
|
||
CREATE TABLE IF NOT EXISTS ecosystem_inquiries (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
reference_code VARCHAR(32) NOT NULL UNIQUE,
|
||
inquiry_type VARCHAR(80) NOT NULL,
|
||
interest_area VARCHAR(120) NOT NULL,
|
||
contact_name VARCHAR(120) NOT NULL,
|
||
organization_name VARCHAR(160) NOT NULL,
|
||
email VARCHAR(160) NOT NULL,
|
||
phone VARCHAR(40) DEFAULT NULL,
|
||
target_date VARCHAR(40) DEFAULT NULL,
|
||
event_scale VARCHAR(80) DEFAULT NULL,
|
||
preferred_location VARCHAR(120) DEFAULT NULL,
|
||
notes TEXT NOT NULL,
|
||
status VARCHAR(30) NOT NULL DEFAULT 'New',
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_status_created (status, created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
SQL;
|
||
}
|
||
|
||
function ensure_inquiry_table(): bool
|
||
{
|
||
static $ensured = false;
|
||
|
||
if ($ensured) {
|
||
return true;
|
||
}
|
||
|
||
$pdo = app_db_connection();
|
||
if (!$pdo) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$pdo->exec(inquiry_table_sql());
|
||
$ensured = true;
|
||
return true;
|
||
} catch (Throwable $exception) {
|
||
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function normalize_multiline(string $value): string
|
||
{
|
||
$normalized = preg_replace("/\r\n?|\r/", "\n", $value);
|
||
$normalized = preg_replace("/\n{3,}/", "\n\n", (string) $normalized);
|
||
return trim((string) $normalized);
|
||
}
|
||
|
||
function validate_inquiry(array $input): array
|
||
{
|
||
$data = [
|
||
'inquiry_type' => trim((string) ($input['inquiry_type'] ?? '')),
|
||
'interest_area' => trim((string) ($input['interest_area'] ?? '')),
|
||
'contact_name' => trim((string) ($input['contact_name'] ?? '')),
|
||
'organization_name' => trim((string) ($input['organization_name'] ?? '')),
|
||
'email' => trim((string) ($input['email'] ?? '')),
|
||
'phone' => trim((string) ($input['phone'] ?? '')),
|
||
'target_date' => trim((string) ($input['target_date'] ?? '')),
|
||
'event_scale' => trim((string) ($input['event_scale'] ?? '')),
|
||
'preferred_location' => trim((string) ($input['preferred_location'] ?? '')),
|
||
'notes' => normalize_multiline((string) ($input['notes'] ?? '')),
|
||
];
|
||
|
||
$errors = [];
|
||
|
||
if ($data['inquiry_type'] === '') {
|
||
$errors['inquiry_type'] = 'Choose the type of collaboration you want to discuss.';
|
||
}
|
||
|
||
if ($data['interest_area'] === '') {
|
||
$errors['interest_area'] = 'Select the package or partnership route that best matches your brief.';
|
||
}
|
||
|
||
if ($data['contact_name'] === '') {
|
||
$errors['contact_name'] = 'Add the primary contact name.';
|
||
}
|
||
|
||
if ($data['organization_name'] === '') {
|
||
$errors['organization_name'] = 'Add the company or organization name.';
|
||
}
|
||
|
||
if ($data['email'] === '' || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||
$errors['email'] = 'Use a valid email address so the team can respond.';
|
||
}
|
||
|
||
if ($data['notes'] === '') {
|
||
$errors['notes'] = 'Describe the program scope, partner intent, or operational need.';
|
||
} elseif (text_length($data['notes']) < 24) {
|
||
$errors['notes'] = 'Add a little more detail so the brief is useful for review.';
|
||
} elseif (text_length($data['notes']) > 900) {
|
||
$errors['notes'] = 'Keep the brief under 900 characters for this first intake step.';
|
||
}
|
||
|
||
if ($data['phone'] !== '' && text_length($data['phone']) > 40) {
|
||
$errors['phone'] = 'Phone number is too long.';
|
||
}
|
||
|
||
if ($data['target_date'] !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['target_date'])) {
|
||
$errors['target_date'] = 'Use a valid preferred date.';
|
||
}
|
||
|
||
return [
|
||
'data' => $data,
|
||
'errors' => $errors,
|
||
];
|
||
}
|
||
|
||
function generate_reference_code(): string
|
||
{
|
||
return 'GBP-' . gmdate('ymd') . '-' . strtoupper(bin2hex(random_bytes(2)));
|
||
}
|
||
|
||
function save_inquiry(array $input): array
|
||
{
|
||
$validated = validate_inquiry($input);
|
||
$data = $validated['data'];
|
||
$errors = $validated['errors'];
|
||
|
||
if ($errors !== []) {
|
||
return [
|
||
'success' => false,
|
||
'errors' => $errors,
|
||
'data' => $data,
|
||
];
|
||
}
|
||
|
||
$inquiry = $data;
|
||
$inquiry['reference_code'] = generate_reference_code();
|
||
$inquiry['status'] = 'New';
|
||
$inquiry['created_at'] = gmdate('Y-m-d H:i:s');
|
||
|
||
if (ensure_inquiry_table()) {
|
||
try {
|
||
$pdo = app_db_connection();
|
||
if ($pdo instanceof PDO) {
|
||
$statement = $pdo->prepare(
|
||
'INSERT INTO ecosystem_inquiries
|
||
(reference_code, inquiry_type, interest_area, contact_name, organization_name, email, phone, target_date, event_scale, preferred_location, notes, status)
|
||
VALUES
|
||
(:reference_code, :inquiry_type, :interest_area, :contact_name, :organization_name, :email, :phone, :target_date, :event_scale, :preferred_location, :notes, :status)'
|
||
);
|
||
|
||
$statement->bindValue(':reference_code', $inquiry['reference_code']);
|
||
$statement->bindValue(':inquiry_type', $inquiry['inquiry_type']);
|
||
$statement->bindValue(':interest_area', $inquiry['interest_area']);
|
||
$statement->bindValue(':contact_name', $inquiry['contact_name']);
|
||
$statement->bindValue(':organization_name', $inquiry['organization_name']);
|
||
$statement->bindValue(':email', $inquiry['email']);
|
||
$statement->bindValue(':phone', $inquiry['phone'] ?: null, $inquiry['phone'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||
$statement->bindValue(':target_date', $inquiry['target_date'] ?: null, $inquiry['target_date'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||
$statement->bindValue(':event_scale', $inquiry['event_scale'] ?: null, $inquiry['event_scale'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||
$statement->bindValue(':preferred_location', $inquiry['preferred_location'] ?: null, $inquiry['preferred_location'] === '' ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||
$statement->bindValue(':notes', $inquiry['notes']);
|
||
$statement->bindValue(':status', $inquiry['status']);
|
||
$statement->execute();
|
||
|
||
$inquiry['id'] = (int) $pdo->lastInsertId();
|
||
|
||
return [
|
||
'success' => true,
|
||
'inquiry' => $inquiry,
|
||
'storage' => 'database',
|
||
];
|
||
}
|
||
} catch (Throwable $exception) {
|
||
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
|
||
}
|
||
}
|
||
|
||
$_SESSION['preview_inquiries'] = $_SESSION['preview_inquiries'] ?? [];
|
||
$_SESSION['preview_inquiry_counter'] = (int) ($_SESSION['preview_inquiry_counter'] ?? 0) + 1;
|
||
$inquiry['id'] = (int) $_SESSION['preview_inquiry_counter'];
|
||
$_SESSION['preview_inquiries'][$inquiry['reference_code']] = $inquiry;
|
||
|
||
return [
|
||
'success' => true,
|
||
'inquiry' => $inquiry,
|
||
'storage' => 'session',
|
||
'warning' => 'Database connectivity is not available, so this submission is stored only for the current preview session.',
|
||
];
|
||
}
|
||
|
||
function session_inquiries(): array
|
||
{
|
||
$items = array_values($_SESSION['preview_inquiries'] ?? []);
|
||
usort($items, static fn (array $left, array $right): int => strcmp($right['created_at'], $left['created_at']));
|
||
return $items;
|
||
}
|
||
|
||
function list_inquiries(): array
|
||
{
|
||
if (ensure_inquiry_table()) {
|
||
try {
|
||
$pdo = app_db_connection();
|
||
if ($pdo instanceof PDO) {
|
||
$statement = $pdo->query('SELECT * FROM ecosystem_inquiries ORDER BY created_at DESC, id DESC LIMIT 50');
|
||
return $statement->fetchAll() ?: [];
|
||
}
|
||
} catch (Throwable $exception) {
|
||
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
|
||
}
|
||
}
|
||
|
||
return session_inquiries();
|
||
}
|
||
|
||
function find_inquiry(?int $id = null, ?string $reference = null): ?array
|
||
{
|
||
if (ensure_inquiry_table()) {
|
||
try {
|
||
$pdo = app_db_connection();
|
||
if ($pdo instanceof PDO) {
|
||
if ($reference !== null && $reference !== '') {
|
||
$statement = $pdo->prepare('SELECT * FROM ecosystem_inquiries WHERE reference_code = :reference LIMIT 1');
|
||
$statement->bindValue(':reference', $reference);
|
||
$statement->execute();
|
||
$found = $statement->fetch();
|
||
return is_array($found) ? $found : null;
|
||
}
|
||
|
||
if ($id !== null && $id > 0) {
|
||
$statement = $pdo->prepare('SELECT * FROM ecosystem_inquiries WHERE id = :id LIMIT 1');
|
||
$statement->bindValue(':id', $id, PDO::PARAM_INT);
|
||
$statement->execute();
|
||
$found = $statement->fetch();
|
||
return is_array($found) ? $found : null;
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
|
||
}
|
||
}
|
||
|
||
foreach (session_inquiries() as $item) {
|
||
if (($reference !== null && $reference !== '' && $item['reference_code'] === $reference) || ($id !== null && (int) $item['id'] === $id)) {
|
||
return $item;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function format_date_label(?string $date): string
|
||
{
|
||
$value = trim((string) $date);
|
||
if ($value === '') {
|
||
return 'To be coordinated';
|
||
}
|
||
|
||
try {
|
||
return (new DateTimeImmutable($value))->format('j M Y');
|
||
} catch (Throwable) {
|
||
return $value;
|
||
}
|
||
}
|