From ef6881fc670b5178517f384500e0d25b5071a08a Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Tue, 12 May 2026 10:41:52 -0400 Subject: [PATCH] gui chnage stage 1 --- .claude/settings.local.json | 7 +- app/Controllers/HomeController.php | 18 +- app/Repositories/CampaignRepository.php | 29 + app/Repositories/CampaignTypeRepository.php | 6 + app/Repositories/JobRepository.php | 6 + app/Repositories/JobTypeRepository.php | 6 + app/ViewModels/HomeIndexViewModel.php | 10 +- app/Views/home/index.php | 136 ++- app/Views/partials/header.php | 3 + public/css/site.css | 1042 ++++++++++++------- public/js/app.js | 188 ++-- 11 files changed, 913 insertions(+), 538 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0f87d20..636f2c9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,12 @@ "PowerShell(php -r \"json_decode\\(file_get_contents\\('d:/Development/PHP/Campaign-Tracker/composer.json'\\), true\\) === null ? print\\('INVALID JSON'\\) : print\\('JSON OK'\\);\")", "PowerShell(php -l \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\\\\core\\\\Database.php\")", "PowerShell(php -l \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\\\\app\\\\Controllers\\\\AuthController.php\")", - "PowerShell(docker compose exec campaign-tracker-app php scripts/debug_sheets.php 2>&1)" + "PowerShell(docker compose exec campaign-tracker-app php scripts/debug_sheets.php 2>&1)", + "Bash(Select-Object -First 30)", + "Bash(Format-Table FullName)", + "Bash(dir /b /s)", + "Bash(findstr \"^app\")", + "PowerShell(cd \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\"; Get-ChildItem -Recurse -Directory | Select-Object -ExpandProperty FullName | Where-Object { $_ -notmatch '\\\\.git|\\\\.claude|node_modules' } | Sort-Object | Select-Object -First 30)" ] } } diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php index 4ef3ab8..12efc7e 100644 --- a/app/Controllers/HomeController.php +++ b/app/Controllers/HomeController.php @@ -4,6 +4,10 @@ declare(strict_types=1); namespace App\Controllers; +use App\Repositories\CampaignRepository; +use App\Repositories\CampaignTypeRepository; +use App\Repositories\JobRepository; +use App\Repositories\JobTypeRepository; use App\ViewModels\HomeIndexViewModel; use Core\Controller; @@ -11,15 +15,19 @@ class HomeController extends Controller { public function index() { + $db = database(); + $model = new HomeIndexViewModel(); - $model->title = 'Campaign Tracker'; - $model->eyebrow = 'PHP MVC application'; - $model->message = 'Manage campaign types and their configurable attributes using a lightweight PHP MVC stack backed by SQL Server.'; - $model->routeExample = '/campaign-types'; + $model->totalCampaignTypes = (new CampaignTypeRepository($db))->count(); + $model->totalCampaigns = (new CampaignRepository($db))->count(); + $model->totalJobTypes = (new JobTypeRepository($db))->count(); + $model->totalJobs = (new JobRepository($db))->count(); + $model->recentCampaigns = (new CampaignRepository($db))->recentWithType(5); + $model->campaignsByType = (new CampaignRepository($db))->countByType(); return $this->view('home.index', [ 'model' => $model, - 'pageTitle' => $model->title, + 'pageTitle' => 'Dashboard', ]); } diff --git a/app/Repositories/CampaignRepository.php b/app/Repositories/CampaignRepository.php index 5842dfe..8a0db99 100644 --- a/app/Repositories/CampaignRepository.php +++ b/app/Repositories/CampaignRepository.php @@ -12,6 +12,35 @@ class CampaignRepository extends Repository protected string $table = 'campaign'; protected string $primaryKey = 'id'; + public function count(): int + { + $row = $this->database->first('SELECT COUNT(*) AS total FROM campaign'); + return (int) ($row['total'] ?? 0); + } + + /** @return list> */ + public function recentWithType(int $limit = 5): array + { + return $this->database->query( + "SELECT TOP ({$limit}) c.id, c.created_at, ct.name AS campaign_type_name + FROM campaign c + INNER JOIN campaign_type ct ON c.campaign_type_id = ct.id + ORDER BY c.id DESC" + ); + } + + /** @return list> */ + public function countByType(): array + { + return $this->database->query( + 'SELECT ct.name AS campaign_type_name, COUNT(c.id) AS campaign_count + FROM campaign_type ct + LEFT JOIN campaign c ON c.campaign_type_id = ct.id + GROUP BY ct.id, ct.name + ORDER BY campaign_count DESC, ct.name ASC' + ); + } + /** * All campaigns joined with their campaign type name, ordered by id desc. * diff --git a/app/Repositories/CampaignTypeRepository.php b/app/Repositories/CampaignTypeRepository.php index f8bbc61..0e58a10 100644 --- a/app/Repositories/CampaignTypeRepository.php +++ b/app/Repositories/CampaignTypeRepository.php @@ -14,6 +14,12 @@ class CampaignTypeRepository extends Repository /** * @return list> */ + public function count(): int + { + $row = $this->database->first('SELECT COUNT(*) AS total FROM campaign_type'); + return (int) ($row['total'] ?? 0); + } + public function allOrderedByName(): array { return $this->database->query( diff --git a/app/Repositories/JobRepository.php b/app/Repositories/JobRepository.php index d36a9a6..9482e21 100644 --- a/app/Repositories/JobRepository.php +++ b/app/Repositories/JobRepository.php @@ -12,6 +12,12 @@ class JobRepository extends Repository protected string $table = 'job'; protected string $primaryKey = 'id'; + public function count(): int + { + $row = $this->database->first('SELECT COUNT(*) AS total FROM job'); + return (int) ($row['total'] ?? 0); + } + /** @return list> */ public function allWithDetails(): array { diff --git a/app/Repositories/JobTypeRepository.php b/app/Repositories/JobTypeRepository.php index c4ee5b6..7a9b4fb 100644 --- a/app/Repositories/JobTypeRepository.php +++ b/app/Repositories/JobTypeRepository.php @@ -11,6 +11,12 @@ class JobTypeRepository extends Repository { protected string $table = 'job_type'; + public function count(): int + { + $row = $this->database->first('SELECT COUNT(*) AS total FROM job_type'); + return (int) ($row['total'] ?? 0); + } + /** @return list> */ public function allOrderedByName(): array { diff --git a/app/ViewModels/HomeIndexViewModel.php b/app/ViewModels/HomeIndexViewModel.php index 458b11f..fea0a45 100644 --- a/app/ViewModels/HomeIndexViewModel.php +++ b/app/ViewModels/HomeIndexViewModel.php @@ -6,8 +6,10 @@ namespace App\ViewModels; class HomeIndexViewModel { - public string $title = ''; - public string $eyebrow = ''; - public string $message = ''; - public string $routeExample = ''; + public int $totalCampaignTypes = 0; + public int $totalCampaigns = 0; + public int $totalJobTypes = 0; + public int $totalJobs = 0; + public array $recentCampaigns = []; + public array $campaignsByType = []; } diff --git a/app/Views/home/index.php b/app/Views/home/index.php index 3820ef7..91eb9df 100644 --- a/app/Views/home/index.php +++ b/app/Views/home/index.php @@ -1,39 +1,109 @@ -
-
- eyebrow) ?> -

title) ?>

-

message) ?>

- -
- Open Campaign Types - See Highlights +campaignsByType as $row) { + if ((int) $row['campaign_count'] > $maxCount) { + $maxCount = (int) $row['campaign_count']; + } +} +?> +
+ +
+
+

Dashboard

+

Overview of your campaign tracking data.

- -
+
+ Campaigns + totalCampaigns) ?> +
+
+ Job Types + totalJobTypes) ?> +
+
+ Jobs + totalJobs) ?> +
+
+ +
+ +
+
+
+

Recent Campaigns

+

The 5 most recently created campaigns.

+
+ View All +
+ + recentCampaigns)): ?> +
+

No campaigns yet.

+

Create your first campaign

+
+ + + + + + + + + + + + recentCampaigns as $row): ?> + + + + + + + + +
IDTypeCreated
#Edit
+ +
+ +
+
+
+

Campaigns by Type

+

Campaign count per campaign type.

+
+ Manage Types +
+ + campaignsByType)): ?> +
+

No campaign types yet.

+

Create your first type

+
+ +
+ campaignsByType as $row): ?> + 0 ? round((int) $row['campaign_count'] / $maxCount * 100) : 0; ?> +
+ + + + + +
+ +
+ +
+ +
-
-
-

Readable by design

-

Small files, explicit routing, and plain PHP views keep the framework approachable for day-to-day work.

-
- -
-

Classic MVC feel

-

Controllers, repositories, and view models stay separate so request handling remains predictable and easy to follow.

-
- -
-

SQL Server ready

-

Typed PHP 8.3 code, Composer autoloading, PDO access, and migration support make the project feel current without becoming heavyweight.

-
diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php index 7d9bc73..0c19165 100644 --- a/app/Views/partials/header.php +++ b/app/Views/partials/header.php @@ -20,6 +20,9 @@ $jsVersion = filemtime(__DIR__ . '/../../../public/js/app.js') ?: time(); <?= e($pageTitle ?? 'Campaign Tracker') ?> + + + diff --git a/public/css/site.css b/public/css/site.css index 606f944..b272f2f 100644 --- a/public/css/site.css +++ b/public/css/site.css @@ -1,19 +1,49 @@ +/* ── Design System Tokens ────────────────────────────────────────────── */ :root { - --page-background: #f4efe7; - --surface: rgba(255, 252, 247, 0.88); - --surface-strong: #fffdf8; - --surface-border: rgba(26, 72, 64, 0.12); - --text-primary: #143631; - --text-secondary: #4f655f; - --accent: #1d7a6d; - --accent-strong: #135c52; - --accent-soft: #daf1ec; - --highlight: #ef7c4d; - --shadow-soft: 0 18px 50px rgba(20, 54, 49, 0.1); - --shadow-card: 0 20px 40px rgba(20, 54, 49, 0.08); -} - -* { + --primary: #1F4E79; + --primary-dark: #163b5c; + --primary-light: #dbeafe; + --secondary: #0F766E; + --accent: #2563EB; + + --success: #2E7D32; + --success-bg: #F0FDF4; + --success-border: #BBF7D0; + --warning: #B45309; + --warning-bg: #FFFBEB; + --warning-border: #FDE68A; + --error: #B91C1C; + --error-bg: #FEF2F2; + --error-border: #FECACA; + --info: #2563EB; + --info-bg: #EFF6FF; + --info-border: #BFDBFE; + --overdue: #7F1D1D; + + --bg: #F7F9FC; + --surface: #FFFFFF; + --surface-raised: #F8FAFC; + --border: #D0D7E2; + --border-strong: #9FB6D6; + + --text: #111827; + --text-secondary: #4B5563; + --text-muted: #6B7280; + + --radius-xs: 4px; + --radius-sm: 6px; + --radius: 8px; + --radius-lg: 10px; + --radius-xl: 12px; + + --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-sm: 0 1px 4px rgba(15, 23, 42, 0.08); + --shadow: 0 4px 12px rgba(15, 23, 42, 0.10); + --shadow-md: 0 8px 24px rgba(15, 23, 42, 0.12); +} + +/* ── Reset / Base ────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } @@ -28,12 +58,13 @@ html { body { margin: 0; min-height: 100vh; - font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande", sans-serif; - color: var(--text-primary); - background: - radial-gradient(circle at top left, rgba(239, 124, 77, 0.18), transparent 28%), - radial-gradient(circle at top right, rgba(29, 122, 109, 0.18), transparent 32%), - linear-gradient(180deg, #f8f2e8 0%, var(--page-background) 48%, #efe6da 100%); + font-family: "Public Sans", "Segoe UI", Arial, sans-serif; + font-size: 14px; + line-height: 22px; + font-weight: 400; + color: var(--text); + background: var(--bg); + overflow-x: hidden; } a { @@ -41,9 +72,22 @@ a { } code { - font-family: Consolas, "Courier New", monospace; + font-family: "IBM Plex Mono", Consolas, "Courier New", monospace; + font-size: 13px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 600; + color: var(--text); } +h1 { font-size: 28px; line-height: 36px; } +h2 { font-size: 22px; line-height: 30px; } +h3 { font-size: 18px; line-height: 26px; } +h4 { font-size: 16px; line-height: 24px; } + +/* ── Layout ──────────────────────────────────────────────────────────── */ .page-shell { min-height: 100vh; display: flex; @@ -51,17 +95,18 @@ code { } .container { - width: min(1120px, calc(100% - 2rem)); + width: min(1200px, calc(100% - 2rem)); margin: 0 auto; } +/* ── Site Header ─────────────────────────────────────────────────────── */ .site-header { position: sticky; top: 0; z-index: 20; - backdrop-filter: blur(14px); - background: rgba(248, 242, 232, 0.78); - border-bottom: 1px solid rgba(20, 54, 49, 0.08); + background: var(--primary); + border-bottom: 1px solid var(--primary-dark); + box-shadow: var(--shadow-sm); } .header-inner { @@ -69,13 +114,14 @@ code { align-items: center; justify-content: space-between; gap: 1rem; - padding: 1rem 0; + padding: 0; + height: 52px; } .brand { display: inline-flex; align-items: center; - gap: 0.85rem; + gap: 10px; text-decoration: none; } @@ -83,44 +129,48 @@ code { display: inline-flex; align-items: center; justify-content: center; - width: 2.75rem; - height: 2.75rem; - border-radius: 0.95rem; - background: linear-gradient(135deg, var(--accent), var(--highlight)); + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: rgba(255, 255, 255, 0.18); color: #fff; - font-weight: 700; - letter-spacing: 0.08em; - box-shadow: var(--shadow-soft); + font-weight: 800; + font-size: 12px; + letter-spacing: 0.04em; + flex-shrink: 0; } .brand-copy { display: flex; flex-direction: column; - line-height: 1.1; + line-height: 1.2; } .brand-copy strong { - font-size: 1rem; + font-size: 14px; + font-weight: 700; + color: #fff; + letter-spacing: 0.01em; } .brand-copy small { - color: var(--text-secondary); - font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + font-size: 11px; text-transform: uppercase; - letter-spacing: 0.14em; + letter-spacing: 0.1em; } .site-nav { display: flex; align-items: center; - gap: 0.6rem; + gap: 2px; flex-wrap: wrap; } .nav-user { - padding: 0 0.4rem; - color: var(--text-secondary); - font-size: 0.88rem; + padding: 0 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 13px; font-weight: 600; } @@ -130,53 +180,59 @@ code { .nav-link { text-decoration: none; - color: var(--text-secondary); + color: rgba(255, 255, 255, 0.75); + font-size: 13px; font-weight: 600; - padding: 0.7rem 1rem; - border-radius: 999px; - transition: background-color 160ms ease, color 160ms ease, transform 160ms ease; + padding: 6px 12px; + border-radius: var(--radius-sm); + transition: background-color 120ms ease, color 120ms ease; } .nav-link:hover, -.nav-link:focus-visible, +.nav-link:focus-visible { + color: #fff; + background: rgba(255, 255, 255, 0.12); +} + .nav-link.is-active { - color: var(--accent-strong); - background: rgba(29, 122, 109, 0.12); - transform: translateY(-1px); + color: #fff; + background: rgba(255, 255, 255, 0.18); } +/* ── Page Content ────────────────────────────────────────────────────── */ .page-content { flex: 1; - padding: 3.5rem 0 4rem; + padding: 32px 0 40px; } .content-stack { display: grid; - gap: 1.5rem; + gap: 24px; } +/* ── Section Heading ─────────────────────────────────────────────────── */ .section-heading { - max-width: 46rem; + max-width: 720px; } .section-heading h1 { - margin: 0.3rem 0 0.8rem; - font-size: clamp(2.4rem, 5vw, 4rem); - line-height: 1; - letter-spacing: -0.04em; + margin: 4px 0 8px; + font-size: 28px; + line-height: 36px; } .section-heading p { margin: 0; color: var(--text-secondary); - line-height: 1.8; - font-size: 1.05rem; + line-height: 22px; + font-size: 14px; } +/* ── Hero (home page) ────────────────────────────────────────────────── */ .hero { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr); - gap: 1.5rem; + gap: 16px; align-items: stretch; } @@ -187,155 +243,141 @@ code { .alert, .empty-state { background: var(--surface); - border: 1px solid var(--surface-border); - box-shadow: var(--shadow-card); + border: 1px solid var(--border); + box-shadow: var(--shadow-sm); } .hero-copy { - padding: 3rem; - border-radius: 2rem; + padding: 32px; + border-radius: var(--radius-lg); } .eyebrow { display: inline-block; - margin-bottom: 1rem; - padding: 0.4rem 0.75rem; + margin-bottom: 16px; + padding: 3px 10px; border-radius: 999px; - background: var(--accent-soft); - color: var(--accent-strong); - font-size: 0.78rem; + background: var(--info-bg); + border: 1px solid var(--info-border); + color: var(--accent); + font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.14em; + letter-spacing: 0.1em; } .hero h1 { - margin: 0; - font-size: clamp(2.8rem, 6vw, 4.8rem); - line-height: 0.98; - letter-spacing: -0.04em; + font-size: clamp(24px, 4vw, 36px); + line-height: 1.15; + letter-spacing: -0.02em; + color: var(--primary); } .hero-text { - max-width: 44rem; - margin: 1.25rem 0 0; - font-size: 1.12rem; - line-height: 1.8; + max-width: 560px; + margin: 16px 0 0; + font-size: 14px; + line-height: 22px; color: var(--text-secondary); } .hero-actions { display: flex; flex-wrap: wrap; - gap: 0.85rem; - margin-top: 2rem; -} - -.button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.9rem 1.35rem; - border-radius: 999px; - text-decoration: none; - font-weight: 700; -} - -.button-primary { - background: linear-gradient(135deg, var(--accent), var(--accent-strong)); - color: #fff; - box-shadow: 0 18px 30px rgba(19, 92, 82, 0.25); -} - -.button-secondary { - background: rgba(29, 122, 109, 0.08); - color: var(--accent-strong); + gap: 8px; + margin-top: 24px; } .hero-panel { display: flex; flex-direction: column; justify-content: space-between; - padding: 2rem; - border-radius: 1.8rem; + padding: 24px; + border-radius: var(--radius-lg); } .panel-label { - margin: 0 0 1rem; - font-size: 0.78rem; + margin: 0 0 12px; + font-size: 11px; font-weight: 700; - letter-spacing: 0.16em; + letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); } .hero-panel code { display: block; - padding: 1rem 1.1rem; - border-radius: 1.2rem; - background: #173d37; - color: #eefbf6; - line-height: 1.7; + padding: 12px 14px; + border-radius: var(--radius); + background: #0f172a; + color: #e2e8f0; + font-family: "IBM Plex Mono", Consolas, monospace; + font-size: 12px; + line-height: 20px; white-space: normal; } .route-callout { - margin-top: 1.5rem; - padding: 1rem 1.1rem; - border-radius: 1.2rem; - background: var(--surface-strong); + margin-top: 16px; + padding: 12px 14px; + border-radius: var(--radius); + background: var(--surface-raised); + border: 1px solid var(--border); } .route-callout span { display: block; - margin-bottom: 0.45rem; + margin-bottom: 6px; color: var(--text-secondary); - font-size: 0.92rem; + font-size: 13px; } .route-callout a { - color: var(--highlight); + color: var(--accent); font-weight: 700; text-decoration: none; } +/* ── Feature Grid ────────────────────────────────────────────────────── */ .feature-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1.25rem; - margin-top: 1.5rem; + gap: 16px; + margin-top: 16px; } .feature-card { - padding: 1.75rem; - border-radius: 1.6rem; + padding: 20px; + border-radius: var(--radius-lg); } .feature-card h2 { margin-top: 0; - margin-bottom: 0.8rem; - font-size: 1.25rem; + margin-bottom: 8px; + font-size: 16px; + line-height: 24px; } .feature-card p { margin: 0; color: var(--text-secondary); - line-height: 1.7; + line-height: 22px; + font-size: 13px; } +/* ── Panels & Controls ───────────────────────────────────────────────── */ .controls-panel, .table-shell { overflow: hidden; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(248, 242, 232, 0.88)), - var(--surface); + background: var(--surface); + border: 1px solid var(--border); } .controls-header { display: flex; align-items: flex-start; justify-content: space-between; - gap: 1rem; + gap: 16px; } .search-row { @@ -348,47 +390,50 @@ code { } .section-panel { - padding: 1.75rem; - border-radius: 1.8rem; + padding: 24px; + border-radius: var(--radius-lg); + min-width: 0; } .panel-header { display: flex; align-items: flex-start; justify-content: space-between; - gap: 1rem; + gap: 16px; flex-wrap: wrap; - margin-bottom: 1.5rem; + margin-bottom: 16px; } .panel-actions { display: flex; align-items: center; - gap: 0.5rem; + gap: 8px; flex-wrap: wrap; } .panel-header h2 { - margin: 0 0 0.45rem; - font-size: 1.45rem; + margin: 0 0 4px; + font-size: 18px; + line-height: 26px; } .panel-header p { margin: 0; color: var(--text-secondary); - line-height: 1.7; + line-height: 22px; + font-size: 13px; } .job-type-table-stack { display: grid; - gap: 1.5rem; + gap: 16px; } .job-type-table-group { display: grid; - gap: 0.85rem; - padding-top: 1.25rem; - border-top: 1px solid rgba(20, 54, 49, 0.1); + gap: 8px; + padding-top: 16px; + border-top: 1px solid var(--border); } .job-type-table-group:first-child { @@ -400,97 +445,63 @@ code { display: flex; align-items: center; justify-content: space-between; - gap: 1rem; + gap: 16px; flex-wrap: wrap; } .job-type-table-heading h3 { margin: 0; - font-size: 1.08rem; + font-size: 15px; + line-height: 22px; } .job-type-table-heading span { color: var(--text-secondary); - font-size: 0.86rem; + font-size: 12px; font-weight: 700; } +/* ── Forms ───────────────────────────────────────────────────────────── */ .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -/* Campaign jobs table — horizontal scroll inside the panel */ -#campaign-jobs-page-table { - overflow-x: auto; - width: 100%; -} - -.import-tabs { - display: flex; - gap: 0.25rem; - margin-bottom: 1.25rem; - border-bottom: 1px solid var(--surface-border); - padding-bottom: 0; -} - -.import-tab { - padding: 0.55rem 1.1rem; - border: none; - background: none; - cursor: pointer; - font: inherit; - font-weight: 600; - color: var(--text-secondary); - border-bottom: 2px solid transparent; - margin-bottom: -1px; - border-radius: 0; - transition: color 120ms, border-color 120ms; -} - -.import-tab:hover { color: var(--accent); } -.import-tab.is-active { color: var(--accent-strong); border-bottom-color: var(--accent-strong); } - -.import-grid { - display: grid; - grid-template-columns: minmax(260px, 1.4fr) minmax(180px, 0.8fr) minmax(180px, 0.8fr); - gap: 1rem; -} - -.import-actions { - margin-top: 1rem; - flex-wrap: wrap; + gap: 16px; } .field { display: grid; - gap: 0.45rem; + gap: 6px; font-weight: 600; + font-size: 13px; } .field span { - font-size: 0.96rem; + font-size: 13px; + color: var(--text); } .input { width: 100%; - padding: 0.95rem 1rem; - border: 1px solid rgba(20, 54, 49, 0.16); - border-radius: 1rem; - background: rgba(255, 255, 255, 0.92); - color: var(--text-primary); + padding: 7px 10px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text); font: inherit; + font-size: 14px; + line-height: 20px; + transition: border-color 120ms ease, box-shadow 120ms ease; } .input:focus { - outline: 2px solid rgba(29, 122, 109, 0.22); - border-color: rgba(29, 122, 109, 0.45); + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); } .field-error { - color: #a43d1f; - font-size: 0.88rem; + color: var(--error); + font-size: 12px; font-weight: 600; } @@ -498,136 +509,313 @@ code { display: flex; justify-content: flex-start; align-items: center; - gap: 0.85rem; + gap: 8px; } +/* ── Buttons ─────────────────────────────────────────────────────────── */ .button { - border: 0; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 7px 16px; + border-radius: var(--radius-sm); + text-decoration: none; + font-family: inherit; + font-size: 13px; + font-weight: 600; + line-height: 20px; + border: 1px solid transparent; cursor: pointer; + transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease; } -.htmx-indicator { - display: none; +.button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } -.htmx-request .htmx-indicator, -.htmx-request.htmx-indicator { - display: inline-flex; +.button-primary { + background: var(--primary); + color: #fff; + border-color: var(--primary-dark); } -.inline-indicator { - color: var(--text-secondary); - font-size: 0.9rem; - font-weight: 600; +.button-primary:hover { + background: var(--primary-dark); +} + +.button-secondary { + background: var(--surface); + color: var(--text); + border-color: var(--border); +} + +.button-secondary:hover { + background: var(--surface-raised); + border-color: var(--border-strong); +} + +.button-danger { + background: var(--error); + color: #fff; + border-color: #991B1B; } +.button-danger:hover, +.button-danger:focus-visible { + background: #991B1B; +} + +.button-sm { + padding: 4px 10px; + font-size: 12px; + border-radius: var(--radius-xs); + border: 1px solid transparent; + cursor: pointer; +} + +/* ── Alerts & Empty States ───────────────────────────────────────────── */ .alert, .empty-state { - padding: 1rem 1.15rem; - border-radius: 1.2rem; + padding: 12px 16px; + border-radius: var(--radius); + font-size: 14px; } .alert-success { - background: rgba(218, 241, 236, 0.92); - color: var(--accent-strong); + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); } .alert-error { - background: rgba(239, 124, 77, 0.14); - color: #8f3518; + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); } .empty-state p { margin: 0; color: var(--text-secondary); - line-height: 1.7; + line-height: 22px; } .empty-state p + p { - margin-top: 0.45rem; + margin-top: 6px; } +/* ── Stats Grid ──────────────────────────────────────────────────────── */ .stats-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.9rem; + gap: 12px; } .stat-card { - padding: 1rem; - border-radius: 1.3rem; - background: rgba(255, 255, 255, 0.72); - border: 1px solid rgba(20, 54, 49, 0.08); + padding: 16px; + border-radius: var(--radius); + background: var(--surface); + border: 1px solid var(--border); + box-shadow: var(--shadow-xs); } .stat-card span { display: block; color: var(--text-secondary); - font-size: 0.82rem; + font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; + font-weight: 600; } .stat-card strong { display: block; - margin-top: 0.45rem; - font-size: 1.7rem; + margin-top: 6px; + font-size: 28px; line-height: 1; + color: var(--primary); } .summary-feature { - margin-top: 1rem; - padding: 1.15rem; - border-radius: 1.3rem; - background: linear-gradient(135deg, rgba(29, 122, 109, 0.12), rgba(239, 124, 77, 0.12)); + margin-top: 16px; + padding: 14px 16px; + border-radius: var(--radius); + background: var(--info-bg); + border: 1px solid var(--info-border); } .summary-label { display: block; color: var(--text-secondary); - font-size: 0.82rem; + font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; + font-weight: 600; } .summary-feature h3 { - margin: 0.55rem 0 0.3rem; - font-size: 1.35rem; + margin: 6px 0 4px; + font-size: 16px; + line-height: 24px; + color: var(--primary); } .summary-feature p { margin: 0; color: var(--text-secondary); + font-size: 13px; +} + +/* ── Dashboard ───────────────────────────────────────────────────────── */ +.stats-grid-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.dashboard-panels { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + align-items: start; +} + +.dashboard-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.dashboard-table th { + text-align: left; + padding: 6px 10px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-secondary); + border-bottom: 1px solid var(--border); +} + +.dashboard-table td { + padding: 8px 10px; + border-bottom: 1px solid var(--border); + color: var(--text-primary); +} + +.dashboard-table tr:last-child td { + border-bottom: none; +} + +.dashboard-table-id { + color: var(--text-secondary); + font-variant-numeric: tabular-nums; + width: 48px; +} + +.dashboard-table-date { + color: var(--text-secondary); + white-space: nowrap; +} + +.dashboard-table-action { + text-align: right; + width: 40px; +} + +.dashboard-table-action a { + color: var(--primary); + font-size: 12px; +} + +.type-breakdown { + display: flex; + flex-direction: column; + gap: 10px; + padding: 4px 0; +} + +.type-breakdown-row { + display: grid; + grid-template-columns: 140px 1fr 36px; + align-items: center; + gap: 10px; + font-size: 13px; +} + +.type-name { + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.type-bar-wrap { + height: 8px; + background: var(--border); + border-radius: 4px; + overflow: hidden; +} + +.type-bar { + display: block; + height: 100%; + background: var(--primary); + border-radius: 4px; + min-width: 4px; + transition: width 0.3s ease; +} + +.type-count { + text-align: right; + font-variant-numeric: tabular-nums; + color: var(--text-secondary); + font-size: 12px; } +@media (max-width: 860px) { + .stats-grid-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dashboard-panels { + grid-template-columns: 1fr; + } +} + +/* ── Table Toolbar ───────────────────────────────────────────────────── */ .table-toolbar { display: flex; align-items: center; justify-content: space-between; - gap: 1rem; - margin-bottom: 1rem; + gap: 16px; + margin-bottom: 12px; flex-wrap: wrap; - padding: 0.9rem 1rem; - border: 1px solid rgba(20, 54, 49, 0.08); - border-radius: 1rem; - background: rgba(255, 255, 255, 0.58); + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); } .table-pill { display: inline-flex; align-items: center; - padding: 0.5rem 0.8rem; + padding: 3px 10px; border-radius: 999px; - background: rgba(29, 122, 109, 0.12); - color: var(--accent-strong); - font-size: 0.82rem; + background: var(--info-bg); + border: 1px solid var(--info-border); + color: var(--accent); + font-size: 12px; font-weight: 700; letter-spacing: 0.04em; } .table-caption { color: var(--text-secondary); - font-size: 0.92rem; + font-size: 12px; +} + +/* ── Tabulator ───────────────────────────────────────────────────────── */ +.tabulator-host { + overflow-x: auto; + width: 100%; } .directory-panel .tabulator-host { @@ -635,24 +823,22 @@ code { } .tabulator-host .tabulator { - border: 1px solid var(--surface-border); - border-radius: 1.35rem; + border: 1px solid var(--border); + border-radius: var(--radius-lg); overflow: hidden; - background: rgba(255, 255, 255, 0.82); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.5), - 0 18px 35px rgba(20, 54, 49, 0.08); + background: var(--surface); + box-shadow: var(--shadow-sm); } .tabulator-host .tabulator-header { - border-bottom: 1px solid rgba(20, 54, 49, 0.08); - background: linear-gradient(180deg, rgba(29, 122, 109, 0.14), rgba(29, 122, 109, 0.08)); + border-bottom: 1px solid var(--border); + background: var(--surface-raised); } .tabulator-host .tabulator-header .tabulator-col { - min-height: 3.25rem; + min-height: 36px; background: transparent; - border-right: 1px solid rgba(20, 54, 49, 0.06); + border-right: 1px solid var(--border); } .tabulator-host .tabulator-header .tabulator-col:last-child { @@ -660,20 +846,20 @@ code { } .tabulator-host .tabulator-header .tabulator-col .tabulator-col-content { - padding: 0.9rem 0.95rem 0.85rem; + padding: 8px 10px; } .tabulator-host .tabulator-header .tabulator-col .tabulator-col-title { - font-size: 0.78rem; - font-weight: 800; - letter-spacing: 0.08em; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; text-transform: uppercase; - color: var(--accent-strong); + color: var(--text-secondary); } .tabulator-host .tabulator-col, .tabulator-host .tabulator-cell { - border-right: 1px solid rgba(20, 54, 49, 0.06); + border-right: 1px solid var(--border); } .tabulator-host .tabulator-row .tabulator-cell:last-child { @@ -681,157 +867,117 @@ code { } .tabulator-host .tabulator-row { - background: rgba(255, 255, 255, 0.96); - border-bottom: 1px solid rgba(20, 54, 49, 0.06); - transition: background-color 160ms ease, transform 160ms ease; + background: var(--surface); + border-bottom: 1px solid var(--border); + transition: background-color 100ms ease; } .tabulator-host .tabulator-row:nth-child(even) { - background: rgba(248, 242, 232, 0.82); + background: var(--surface-raised); } .tabulator-host .tabulator-row:hover { - background: rgba(218, 241, 236, 0.72); + background: var(--info-bg); } .tabulator-host .tabulator-row.tabulator-selected { - background: rgba(29, 122, 109, 0.18); + background: var(--primary-light); } .tabulator-host .tabulator-cell { - padding: 0.95rem 0.95rem; - font-size: 0.96rem; - line-height: 1.4; + padding: 8px 10px; + font-size: 13px; + line-height: 20px; } .tabulator-host .tabulator-row .tabulator-cell:first-child { - font-weight: 700; - color: var(--text-primary); + font-weight: 600; + color: var(--text); } .tabulator-host .tabulator-footer { - padding: 0.55rem 0.7rem; - background: rgba(255, 255, 255, 0.88); - border-top: 1px solid rgba(20, 54, 49, 0.08); + padding: 6px 8px; + background: var(--surface-raised); + border-top: 1px solid var(--border); } .tabulator-host .tabulator-footer .tabulator-paginator { font-family: inherit; + font-size: 13px; } .tabulator-host .tabulator-footer .tabulator-page { - margin: 0 0.2rem; - padding: 0.45rem 0.7rem; - border: 1px solid rgba(20, 54, 49, 0.1); - border-radius: 0.8rem; - background: rgba(255, 255, 255, 0.9); + margin: 0 2px; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: var(--radius-xs); + background: var(--surface); color: var(--text-secondary); - font-weight: 700; + font-weight: 600; + font-size: 12px; } .tabulator-host .tabulator-footer .tabulator-page.active, .tabulator-host .tabulator-footer .tabulator-page:hover { - background: linear-gradient(135deg, var(--accent), var(--accent-strong)); - border-color: transparent; + background: var(--primary); + border-color: var(--primary-dark); color: #fff; } .tabulator-host .tabulator-footer .tabulator-page:disabled { - opacity: 0.45; + opacity: 0.4; } .tabulator-host .tabulator-placeholder { - padding: 2.5rem 1rem; + padding: 32px 16px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 600; +} + +/* ── HTMX Indicators ─────────────────────────────────────────────────── */ +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator, +.htmx-request.htmx-indicator { + display: inline-flex; +} + +.inline-indicator { color: var(--text-secondary); - font-size: 1rem; + font-size: 13px; font-weight: 600; } +/* ── Site Footer ─────────────────────────────────────────────────────── */ .site-footer { margin-top: auto; - border-top: 1px solid rgba(20, 54, 49, 0.08); - background: rgba(255, 252, 247, 0.72); + border-top: 1px solid var(--border); + background: var(--surface); } .footer-inner { display: flex; justify-content: space-between; - gap: 1rem; - padding: 1.25rem 0 2rem; - color: var(--text-secondary); - font-size: 0.95rem; + gap: 16px; + padding: 16px 0 24px; + color: var(--text-muted); + font-size: 12px; + line-height: 18px; } .footer-inner p { margin: 0; } -@media (max-width: 860px) { - .header-inner, - .footer-inner { - flex-direction: column; - align-items: flex-start; - } - - .hero, - .feature-grid { - grid-template-columns: 1fr; - } - - .controls-header, - .table-toolbar { - flex-direction: column; - align-items: flex-start; - } - - .hero-copy, - .hero-panel { - padding: 2rem; - } - - .form-grid { - grid-template-columns: 1fr; - } - - .import-grid { - grid-template-columns: 1fr; - } - - .stats-grid { - grid-template-columns: 1fr; - } - - .page-content { - padding-top: 2rem; - } -} - -@media (max-width: 560px) { - .container { - width: min(100% - 1.25rem, 1120px); - } - - .site-nav { - width: 100%; - } - - .nav-link { - width: 100%; - text-align: center; - } - - .hero h1 { - font-size: 2.5rem; - } -} - -/* ── Campaign Types ─────────────────────────────────────────────────── */ - +/* ── Page Toolbar ────────────────────────────────────────────────────── */ .page-toolbar { display: flex; align-items: flex-start; justify-content: space-between; - gap: 1.5rem; + gap: 16px; flex-wrap: wrap; } @@ -840,77 +986,58 @@ code { } .page-toolbar .section-heading h1 { - margin: 0 0 0.4rem; -} - -.button-danger { - background: linear-gradient(135deg, #c0392b, #962d22); - color: #fff; - box-shadow: 0 8px 20px rgba(192, 57, 43, 0.28); - border: none; - cursor: pointer; -} - -.button-danger:hover, -.button-danger:focus-visible { - background: linear-gradient(135deg, #d44637, #c0392b); -} - -.button-sm { - padding: 0.4rem 0.85rem; - font-size: 0.82rem; - border-radius: 999px; - border: none; - cursor: pointer; + margin: 0 0 4px; } +/* ── Campaign Types / Import Forms ───────────────────────────────────── */ .ct-form { display: grid; - gap: 2rem; + gap: 24px; } .form-section { display: grid; - gap: 1rem; + gap: 12px; } .form-section h3 { margin: 0; - font-size: 1.05rem; + font-size: 15px; + line-height: 22px; } .attributes-header { display: flex; flex-direction: column; - gap: 0.25rem; + gap: 4px; } .attributes-hint { margin: 0; color: var(--text-secondary); - font-size: 0.9rem; + font-size: 13px; } .attribute-list { display: grid; - gap: 0.6rem; + gap: 6px; } .attribute-row { display: flex; align-items: flex-end; - gap: 0.75rem; + gap: 8px; flex-wrap: wrap; } .attr-drag-handle { cursor: grab; - padding: 0 0.3rem; - color: var(--text-secondary); - font-size: 1.25rem; + padding: 0 4px; + color: var(--text-muted); + font-size: 16px; user-select: none; align-self: flex-end; - padding-bottom: 0.6rem; + padding-bottom: 8px; line-height: 1; } @@ -924,8 +1051,8 @@ code { .attribute-row.is-drag-over { outline: 2px dashed var(--accent); - border-radius: 0.8rem; - background: var(--accent-soft); + border-radius: var(--radius-sm); + background: var(--info-bg); } .attribute-order-field { @@ -951,42 +1078,147 @@ code { padding-bottom: 0.1rem; } -.field-full { - width: 100%; -} - .input-error { - border-color: #c0392b !important; + border-color: var(--error) !important; } .required-mark { - color: #c0392b; + color: var(--error); } .delete-zone { - margin-top: 2.5rem; - padding-top: 1.5rem; - border-top: 1px solid rgba(192, 57, 43, 0.2); + margin-top: 32px; + padding-top: 16px; + border-top: 1px solid var(--error-border); } .delete-zone h4 { - margin: 0 0 0.35rem; - color: #c0392b; - font-size: 0.95rem; + margin: 0 0 4px; + color: var(--error); + font-size: 14px; } .delete-zone p { - margin: 0 0 1rem; + margin: 0 0 12px; color: var(--text-secondary); - font-size: 0.88rem; + font-size: 13px; } .attr-summary { color: var(--text-secondary); - font-size: 0.88rem; + font-size: 13px; } .attr-empty { + color: var(--text-muted); + opacity: 0.6; +} + +/* ── Import Tabs ─────────────────────────────────────────────────────── */ +.import-tabs { + display: flex; + gap: 2px; + margin-bottom: 16px; + border-bottom: 1px solid var(--border); + padding-bottom: 0; +} + +.import-tab { + padding: 6px 14px; + border: none; + background: none; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 600; color: var(--text-secondary); - opacity: 0.45; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + border-radius: 0; + transition: color 120ms, border-color 120ms; +} + +.import-tab:hover { color: var(--accent); } +.import-tab.is-active { color: var(--primary); border-bottom-color: var(--primary); } + +.import-grid { + display: grid; + grid-template-columns: minmax(260px, 1.4fr) minmax(180px, 0.8fr) minmax(180px, 0.8fr); + gap: 16px; +} + +.import-actions { + margin-top: 16px; + flex-wrap: wrap; +} + +/* Campaign jobs table — horizontal scroll inside the panel */ +#campaign-jobs-page-table { + overflow-x: auto; + width: 100%; +} + +/* ── Responsive ──────────────────────────────────────────────────────── */ +@media (max-width: 860px) { + .header-inner, + .footer-inner { + flex-direction: column; + align-items: flex-start; + } + + .header-inner { + height: auto; + padding: 12px 0; + } + + .hero, + .feature-grid { + grid-template-columns: 1fr; + } + + .controls-header, + .table-toolbar { + flex-direction: column; + align-items: flex-start; + } + + .hero-copy, + .hero-panel { + padding: 24px; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .import-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .page-content { + padding-top: 24px; + } +} + +@media (max-width: 560px) { + .container { + width: min(100% - 1rem, 1200px); + } + + .site-nav { + width: 100%; + } + + .nav-link { + width: 100%; + text-align: center; + } + + .hero h1 { + font-size: 22px; + } } diff --git a/public/js/app.js b/public/js/app.js index 5b11ae3..e9193d4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,5 +1,9 @@ // ── Shared util ─────────────────────────────────────────────────────────────── +const PAGE_SIZES = [10, 25, 50, 100]; +const PAGE_SIZES_SM = [5, 10, 25, 50]; + + function _postDelete(action) { const form = document.createElement('form'); form.method = 'POST'; @@ -50,10 +54,23 @@ window.campaignTypeTable = function () { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No campaign types found.', initialSort: [{ column: 'name', dir: 'asc' }], columns: [ + { + title: 'Actions', + field: 'id', + width: 160, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + const id = cell.getValue(); + return 'Edit ' + + ''; + }, + }, { title: 'Name', field: 'name', minWidth: 200 }, { title: 'Attributes', @@ -67,18 +84,6 @@ window.campaignTypeTable = function () { }, { title: 'Count', field: 'attribute_count', hozAlign: 'center', width: 80 }, { title: 'Created', field: 'created_at', minWidth: 160 }, - { - title: 'Actions', - field: 'id', - width: 160, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - const id = cell.getValue(); - return 'Edit ' + - ''; - }, - }, ], }); }, @@ -200,6 +205,7 @@ window.campaignTable = function () { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No campaigns found.', initialSort: [{ column: 'campaign_type_name', dir: 'asc' }], @@ -275,6 +281,19 @@ window.campaignTable = function () { columnsForAttributes(attributes) { const columns = [ + { + title: 'Actions', + field: 'id', + width: 230, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + const id = cell.getValue(); + return 'Jobs ' + + 'Edit ' + + ''; + }, + }, { title: 'Campaign Type', field: 'campaign_type_name', @@ -297,20 +316,7 @@ window.campaignTable = function () { }); columns.push( - { title: 'Created', field: 'created_at', minWidth: 160, headerFilter: 'input' }, - { - title: 'Actions', - field: 'id', - width: 230, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - const id = cell.getValue(); - return 'Jobs ' + - 'Edit ' + - ''; - }, - } + { title: 'Created', field: 'created_at', minWidth: 160, headerFilter: 'input' } ); return columns; @@ -387,6 +393,7 @@ window.campaignTable = function () { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No jobs found for this campaign.', initialSort: [{ column: 'job_type_name', dir: 'asc' }], @@ -446,6 +453,16 @@ window.campaignTable = function () { jobColumnsForAttributes(attributes) { const columns = [ + { + title: 'Actions', + field: 'edit_url', + width: 90, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + return 'Edit'; + }, + }, { title: 'Job ID', field: 'id', width: 90, hozAlign: 'center', headerFilter: 'input' }, { title: 'Campaign ID', field: 'campaign_id', width: 120, hozAlign: 'center', headerFilter: 'input' }, { title: 'Job Type ID', field: 'job_type_id', width: 120, hozAlign: 'center', headerFilter: 'input' }, @@ -467,17 +484,7 @@ window.campaignTable = function () { columns.push( { title: 'Created', field: 'created_at', minWidth: 160, headerFilter: 'input' }, - { title: 'Updated', field: 'updated_at', minWidth: 160, headerFilter: 'input' }, - { - title: 'Actions', - field: 'edit_url', - width: 90, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - return 'Edit'; - }, - } + { title: 'Updated', field: 'updated_at', minWidth: 160, headerFilter: 'input' } ); return columns; @@ -565,6 +572,7 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No jobs found for this campaign.', initialSort: [{ column: 'job_type_name', dir: 'asc' }], @@ -644,6 +652,16 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) { columnsForAttributes(attributes) { const columns = [ + { + title: 'Actions', + field: 'edit_url', + width: 90, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + return 'Edit'; + }, + }, { title: 'Job ID', field: 'id', width: 90, hozAlign: 'center', headerFilter: 'input' }, { title: 'Campaign ID', field: 'campaign_id', width: 120, hozAlign: 'center', headerFilter: 'input' }, { title: 'Campaign Type', field: 'campaign_type_name', minWidth: 160, headerFilter: 'input' }, @@ -666,17 +684,7 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) { columns.push( { title: 'Created', field: 'created_at', minWidth: 160, headerFilter: 'input' }, - { title: 'Updated', field: 'updated_at', minWidth: 160, headerFilter: 'input' }, - { - title: 'Actions', - field: 'edit_url', - width: 90, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - return 'Edit'; - }, - } + { title: 'Updated', field: 'updated_at', minWidth: 160, headerFilter: 'input' } ); return columns; @@ -1020,6 +1028,7 @@ window.campaignJobsTable = function (campaignId) { pagination: true, paginationMode: 'local', paginationSize: 5, + paginationSizeSelector: PAGE_SIZES_SM, movableColumns: true, placeholder: 'No jobs found for this job type.', initialSort: [{ column: 'created_at', dir: 'desc' }], @@ -1029,7 +1038,18 @@ window.campaignJobsTable = function (campaignId) { }, columnsForGroup(group) { - const columns = group.attributes.map((attr, index) => ({ + const actions = { + title: 'Actions', + field: 'edit_url', + width: 90, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + return 'Edit'; + }, + }; + + const attrColumns = group.attributes.map((attr, index) => ({ title: attr.name, field: 'attr_' + index, minWidth: 150, @@ -1039,25 +1059,11 @@ window.campaignJobsTable = function (campaignId) { }, })); - if (columns.length === 0) { - columns.push({ title: 'Job ID', field: 'id', width: 90, hozAlign: 'center' }); + if (attrColumns.length === 0) { + attrColumns.push({ title: 'Job ID', field: 'id', width: 90, hozAlign: 'center' }); } - columns.push( - { title: 'Created', field: 'created_at', minWidth: 160 }, - { - title: 'Actions', - field: 'edit_url', - width: 90, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - return 'Edit'; - }, - } - ); - - return columns; + return [actions, ...attrColumns, { title: 'Created', field: 'created_at', minWidth: 160 }]; }, destroyTables() { @@ -1094,10 +1100,23 @@ window.jobTypeTable = function () { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No job types found.', initialSort: [{ column: 'name', dir: 'asc' }], columns: [ + { + title: 'Actions', + field: 'id', + width: 160, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + const id = cell.getValue(); + return 'Edit ' + + ''; + }, + }, { title: 'Name', field: 'name', minWidth: 200 }, { title: 'Attributes', @@ -1111,18 +1130,6 @@ window.jobTypeTable = function () { }, { title: 'Count', field: 'attribute_count', hozAlign: 'center', width: 80 }, { title: 'Created', field: 'created_at', minWidth: 160 }, - { - title: 'Actions', - field: 'id', - width: 160, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - const id = cell.getValue(); - return 'Edit ' + - ''; - }, - }, ], }); }, @@ -1218,10 +1225,23 @@ window.jobTable = function () { pagination: true, paginationMode: 'local', paginationSize: 10, + paginationSizeSelector: PAGE_SIZES, movableColumns: true, placeholder: 'No jobs found.', initialSort: [{ column: 'job_type_name', dir: 'asc' }], columns: [ + { + title: 'Actions', + field: 'id', + width: 160, + hozAlign: 'center', + headerSort: false, + formatter: function (cell) { + const id = cell.getValue(); + return 'Edit ' + + ''; + }, + }, { title: 'Campaign', field: 'campaign_type_name', minWidth: 160 }, { title: 'Job Type', field: 'job_type_name', minWidth: 160 }, { @@ -1235,18 +1255,6 @@ window.jobTable = function () { }, }, { title: 'Created', field: 'created_at', minWidth: 160 }, - { - title: 'Actions', - field: 'id', - width: 160, - hozAlign: 'center', - headerSort: false, - formatter: function (cell) { - const id = cell.getValue(); - return 'Edit ' + - ''; - }, - }, ], }); },