From 7cd27ee2ca9abcfee05a41f0d570d1f5698678cd Mon Sep 17 00:00:00 2001 From: Lumerel Deploy Date: Tue, 17 Feb 2026 03:04:08 +0000 Subject: [PATCH] Deploy from Lumerel --- .docker/99-migrate.sh | 54 ++ .dockerignore | 3 + Dockerfile | 23 + composer.json | 13 + migrate.php | 34 + .../001_create_onboarding_sessions_table.php | 30 + public/dashboard.php | 339 ++++++++ public/index.php | 823 ++++++++++++++++++ public/save.php | 111 +++ src/db.php | 16 + 10 files changed, 1446 insertions(+) create mode 100644 .docker/99-migrate.sh create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 composer.json create mode 100644 migrate.php create mode 100644 migrations/001_create_onboarding_sessions_table.php create mode 100644 public/dashboard.php create mode 100644 public/index.php create mode 100644 public/save.php create mode 100644 src/db.php diff --git a/.docker/99-migrate.sh b/.docker/99-migrate.sh new file mode 100644 index 0000000..66b83d4 --- /dev/null +++ b/.docker/99-migrate.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# IMPORTANT: This script is sourced (not executed) by the webdevops entrypoint. +# Never use "exit" — it would kill the parent entrypoint and prevent supervisord from starting. +# Use "return" instead to leave this script without affecting the parent. + +echo ">>> Waiting for database connection..." +echo ">>> DB_HOST=${DB_HOST:-NOT SET}" +echo ">>> DB_PORT=${DB_PORT:-NOT SET}" +echo ">>> DB_DATABASE=${DB_DATABASE:-NOT SET}" +echo ">>> DB_USERNAME=${DB_USERNAME:-NOT SET}" +echo ">>> DB_PASSWORD is $([ -n "$DB_PASSWORD" ] && echo 'set' || echo 'NOT SET')" + +if [ -z "$DB_HOST" ] || [ -z "$DB_USERNAME" ]; then + echo ">>> ERROR: DB_HOST or DB_USERNAME not set. Skipping migrations." + return 0 2>/dev/null || true +fi + +_migrate_max_retries=30 +_migrate_count=0 +_migrate_done=false + +while [ $_migrate_count -lt $_migrate_max_retries ]; do + if php -r ' + try { + $host = getenv("DB_HOST"); + $port = getenv("DB_PORT") ?: "3306"; + $user = getenv("DB_USERNAME"); + $pass = getenv("DB_PASSWORD"); + new PDO("mysql:host=$host;port=$port", $user, $pass, [PDO::ATTR_TIMEOUT => 3]); + exit(0); + } catch (Throwable $e) { + fwrite(STDERR, "PDO error: " . $e->getMessage() . "\n"); + exit(1); + } + ' 2>&1; then + echo ">>> Database is reachable. Running migrations..." + if php /app/migrate.php; then + echo ">>> Migrations completed successfully." + else + echo ">>> WARNING: migrate.php exited with code $?" + fi + _migrate_done=true + break + fi + + _migrate_count=$((_migrate_count + 1)) + echo ">>> Waiting for database... attempt $_migrate_count/$_migrate_max_retries" + sleep 2 +done + +if [ "$_migrate_done" = false ]; then + echo ">>> WARNING: Could not connect to database after $_migrate_max_retries attempts. Skipping migrations." +fi \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..df335d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app/public +ARG CACHE_BUST=1771297448 +COPY . /app +RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \ + && echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf +RUN set -e; if [ -f /app/composer.json ]; then \ + echo ">>> composer.json found, installing dependencies..."; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \ + ls -la /app/vendor/autoload.php; \ + else \ + echo ">>> No composer.json found, skipping composer install"; \ + fi +RUN set -e; if [ -f /app/package.json ]; then \ + echo ">>> package.json found, installing node dependencies..."; \ + apk add --no-cache nodejs npm; \ + cd /app && npm install --production; \ + else \ + echo ">>> No package.json found, skipping npm install"; \ + fi +COPY --chmod=755 .docker/99-migrate.sh /opt/docker/provision/entrypoint.d/99-migrate.sh +RUN chown -R application:application /app \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2a0fe79 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "todd-low-media/onboarding", + "description": "Interactive onboarding portal for new customers", + "type": "project", + "require": { + "php": ">=7.4" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..1c1b701 --- /dev/null +++ b/migrate.php @@ -0,0 +1,34 @@ +exec(" + CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL UNIQUE, + ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) +"); + +// Get already-run migrations +$stmt = $pdo->query("SELECT migration FROM migrations"); +$ranMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN); + +// Scan and run new migrations +$migrationFiles = glob(__DIR__ . '/migrations/*.php'); +sort($migrationFiles); + +foreach ($migrationFiles as $file) { + $migrationName = basename($file); + + if (!in_array($migrationName, $ranMigrations)) { + echo "Running migration: $migrationName\n"; + require $file; + + $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)")->execute([$migrationName]); + echo "Completed: $migrationName\n"; + } +} + +echo "All migrations completed.\n"; diff --git a/migrations/001_create_onboarding_sessions_table.php b/migrations/001_create_onboarding_sessions_table.php new file mode 100644 index 0000000..e9ef933 --- /dev/null +++ b/migrations/001_create_onboarding_sessions_table.php @@ -0,0 +1,30 @@ +exec(" + CREATE TABLE IF NOT EXISTS onboarding_sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + session_token VARCHAR(64) UNIQUE NOT NULL, + company_name VARCHAR(255), + company_logo VARCHAR(255), + primary_color VARCHAR(7), + secondary_color VARCHAR(7), + industry VARCHAR(100), + company_tagline TEXT, + quote_format VARCHAR(50), + payment_terms VARCHAR(100), + quote_validity_days INT, + show_itemized_pricing BOOLEAN DEFAULT 1, + include_terms_conditions BOOLEAN DEFAULT 1, + contact_name VARCHAR(255), + contact_email VARCHAR(255), + contact_phone VARCHAR(50), + contact_address TEXT, + website VARCHAR(255), + current_step INT DEFAULT 1, + completed BOOLEAN DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_session_token (session_token), + INDEX idx_completed (completed) + ) +"); diff --git a/public/dashboard.php b/public/dashboard.php new file mode 100644 index 0000000..030e778 --- /dev/null +++ b/public/dashboard.php @@ -0,0 +1,339 @@ +prepare("SELECT * FROM onboarding_sessions WHERE session_token = ?"); +$stmt->execute([$token]); +$session = $stmt->fetch(); + +if (!$session || !$session['completed']) { + header('Location: index.php'); + exit; +} +?> + + + + + + Dashboard - <?= htmlspecialchars($session['company_name']) ?> + + + +
+
+
+

+

+
+ ✓ Setup Complete +
+
+ +
+
+

Welcome, ! 👋

+

Your onboarding is complete and your account is ready to go. Below you'll find quick access to key features and your current settings.

+
+ +
+
+
📄
+

Create Quote

+

Start building your first professional quote with your custom branding and preferences.

+ New Quote → +
+ +
+
🎨
+

Customize Templates

+

Personalize your quote templates with your brand colors, logo, and messaging.

+ Edit Templates → +
+ +
+
👥
+

Manage Clients

+

Add and organize your client contacts for faster quote generation.

+ View Clients → +
+ +
+
📊
+

View Reports

+

Track your quotes, conversions, and revenue with detailed analytics.

+ See Reports → +
+ +
+
⚙️
+

Account Settings

+

Update your company information, preferences, and integrations.

+ Manage Settings → +
+ +
+
💬
+

Get Support

+

Need help? Our support team is here to assist you with any questions.

+ Contact Support → +
+
+ +
+

Your Current Settings

+ +
+ Company Name + +
+ +
+ Industry + +
+ +
+ Primary Brand Color + + + + +
+ +
+ Secondary Brand Color + + + + +
+ +
+ Quote Format + +
+ +
+ Payment Terms + +
+ +
+ Quote Validity + days +
+ +
+ Contact Email + +
+ +
+ Contact Phone + +
+ + +
+ Website + + + + + +
+ +
+
+ + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..39e4eab --- /dev/null +++ b/public/index.php @@ -0,0 +1,823 @@ +prepare("INSERT INTO onboarding_sessions (session_token) VALUES (?)"); + $stmt->execute([$_SESSION['onboarding_token']]); +} + +$token = $_SESSION['onboarding_token']; + +// Fetch current session data +$stmt = $pdo->prepare("SELECT * FROM onboarding_sessions WHERE session_token = ?"); +$stmt->execute([$token]); +$session = $stmt->fetch(); + +$currentStep = $session['current_step'] ?? 1; +$completed = $session['completed'] ?? 0; +?> + + + + + + Welcome! Let's Get Started + + + +
+ +
+

🎉 Welcome Aboard!

+

Let's set up your account in just a few simple steps

+
+
+
+
+ +
+
+
1 ? '' : '1' ?>
+
Company Branding
+
+
+
2 ? '' : '2' ?>
+
Quote Preferences
+
+
+
3 ? '' : '3' ?>
+
Contact Info
+
+
+
4
+
Review
+
+
+ +
+
+ +
+

Tell us about your company

+ +
+ + +
+ +
+
+ + +
This will be used for headers and buttons
+
+ +
+ + +
Used for accents and highlights
+
+
+ +
+ + +
+ +
+ + +
A brief description that appears on your quotes
+
+
+ + +
+

Customize your quotes

+ +
+ + +
+ +
+
+ + +
+ +
+ + +
How long quotes remain valid
+
+
+ +
+
+ > + +
+
+ +
+
+ > + +
+
+
+ + +
+

Your contact details

+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+
+ + +
+

Review your information

+ +
+

Company Branding

+
+ Company Name + +
+
+ Primary Color + + + + +
+
+ Secondary Color + + + + +
+
+ Industry + +
+
+ +
+

Quote Preferences

+
+ Format + +
+
+ Payment Terms + +
+
+ Valid For + days +
+
+ +
+

Contact Information

+
+ Name + +
+
+ Email + +
+
+ Phone + +
+
+
+ +
+ + + +
+
+
+ +
+
🎉
+

All Set! Welcome to the Team

+

Your account has been successfully configured. You're ready to start creating beautiful quotes and managing your business.

+ +
+

What's Next?

+

+ • Access your dashboard to create your first quote
+ • Customize templates with your branding
+ • Invite team members to collaborate
+ • Explore advanced features and integrations +

+
+ + +
+ +
+ + + + diff --git a/public/save.php b/public/save.php new file mode 100644 index 0000000..ecf97a2 --- /dev/null +++ b/public/save.php @@ -0,0 +1,111 @@ + 'No session found']); + exit; +} + +$token = $_SESSION['onboarding_token']; + +try { + // Prepare update data + $updates = []; + $params = []; + + // Company branding fields + if (isset($_POST['company_name'])) { + $updates[] = 'company_name = ?'; + $params[] = $_POST['company_name']; + } + if (isset($_POST['primary_color'])) { + $updates[] = 'primary_color = ?'; + $params[] = $_POST['primary_color']; + } + if (isset($_POST['secondary_color'])) { + $updates[] = 'secondary_color = ?'; + $params[] = $_POST['secondary_color']; + } + if (isset($_POST['industry'])) { + $updates[] = 'industry = ?'; + $params[] = $_POST['industry']; + } + if (isset($_POST['company_tagline'])) { + $updates[] = 'company_tagline = ?'; + $params[] = $_POST['company_tagline']; + } + + // Quote preferences fields + if (isset($_POST['quote_format'])) { + $updates[] = 'quote_format = ?'; + $params[] = $_POST['quote_format']; + } + if (isset($_POST['payment_terms'])) { + $updates[] = 'payment_terms = ?'; + $params[] = $_POST['payment_terms']; + } + if (isset($_POST['quote_validity_days'])) { + $updates[] = 'quote_validity_days = ?'; + $params[] = intval($_POST['quote_validity_days']); + } + + // Checkboxes (handle unchecked state) + $updates[] = 'show_itemized_pricing = ?'; + $params[] = isset($_POST['show_itemized_pricing']) ? 1 : 0; + + $updates[] = 'include_terms_conditions = ?'; + $params[] = isset($_POST['include_terms_conditions']) ? 1 : 0; + + // Contact information fields + if (isset($_POST['contact_name'])) { + $updates[] = 'contact_name = ?'; + $params[] = $_POST['contact_name']; + } + if (isset($_POST['contact_email'])) { + $updates[] = 'contact_email = ?'; + $params[] = $_POST['contact_email']; + } + if (isset($_POST['contact_phone'])) { + $updates[] = 'contact_phone = ?'; + $params[] = $_POST['contact_phone']; + } + if (isset($_POST['contact_address'])) { + $updates[] = 'contact_address = ?'; + $params[] = $_POST['contact_address']; + } + if (isset($_POST['website'])) { + $updates[] = 'website = ?'; + $params[] = $_POST['website']; + } + + // Update current step + if (isset($_POST['current_step'])) { + $updates[] = 'current_step = ?'; + $params[] = intval($_POST['current_step']); + } + + // Mark as completed if requested + if (isset($_POST['complete'])) { + $updates[] = 'completed = 1'; + } + + if (!empty($updates)) { + $sql = "UPDATE onboarding_sessions SET " . implode(', ', $updates) . " WHERE session_token = ?"; + $params[] = $token; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => true, 'message' => 'No updates']); + } + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Database error: ' . $e->getMessage()]); +} diff --git a/src/db.php b/src/db.php new file mode 100644 index 0000000..295845f --- /dev/null +++ b/src/db.php @@ -0,0 +1,16 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false + ] + ); +} catch (PDOException $e) { + die('Database connection failed: ' . $e->getMessage()); +}