diff --git a/.abacusai/config.json b/.abacusai/config.json new file mode 100644 index 0000000..ade5063 --- /dev/null +++ b/.abacusai/config.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git *)", + "Bash(git --no-pager log --oneline -8 2>&1 || true)", + "Bash(git --no-pager diff -- app/Controllers/ApiProxyController.php routes/web.php app/Controllers/JobTypeController.php app/V…)" + ] + } +} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 494e663..9a03ddb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,8 @@ "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)", - "Bash(git -C \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" remote get-url origin)" + "Bash(git -C \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" remote get-url origin)", + "PowerShell(Get-ChildItem -Path \"d:\\\\Development\\\\PHP\\\\Campaign-Tracker\" -Recurse -Directory -ErrorAction SilentlyContinue | Select-Object -First 30 | ForEach-Object { $_.FullName })" ] } } diff --git a/.env_prod b/.env_prod new file mode 100644 index 0000000..2e0e03c --- /dev/null +++ b/.env_prod @@ -0,0 +1,19 @@ +APP_ENV=local +APP_DEBUG=true + +DB_HOST=sqlserver +DB_PORT=1433 +DB_DATABASE=Campaign_Tracker +DB_USERNAME=sa +DB_PASSWORD=Dev_Password123! + +# ── Keycloak ─────────────────────────────────────────────────────────────────── +# KEYCLOAK_BASE_URL: Base URL of your Keycloak server. +# Keycloak 17+ (no /auth prefix): http://localhost:8080 +# Keycloak < 17 (has /auth prefix): http://localhost:8080/auth +KEYCLOAK_BASE_URL=http://kci-app01.ntp.kentcommunications.com:8180/ +KEYCLOAK_REALM=KCI +KEYCLOAK_CLIENT_ID=canopy-web +KEYCLOAK_CLIENT_SECRET=LHWXp5UUuES00Dz2iCjTJJgX9su6co0y +KEYCLOAK_REDIRECT_URI=http://192.168.1.200:8801/auth/callback +KEYCLOAK_LOGOUT_REDIRECT_URI=http://192.168.1.200:8801/login diff --git a/AGENTS.md b/AGENTS.md index 3f6550b..8cd361b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1311,4 +1311,21 @@ class MyDashboardViewModel } ``` -Populate it in the controller using private `repo()` methods or inline `new Repository(database())` calls — consistent with how other controllers are written in this project. \ No newline at end of file +Populate it in the controller using private `repo()` methods or inline `new Repository(database())` calls — consistent with how other controllers are written in this project. + +--- + +## Clean JavaScript Practices (Distilled) + +Source: https://medium.com/@onix_react/best-practices-for-writing-clean-javascript-code-a4e5755de69a + +- Prefer `const` and `let` over `var` to avoid function-scoped surprises. +- Keep scope tight and avoid globals to prevent hidden coupling and collisions. +- Use small, focused functions and clear names to make intent obvious. +- Prefer arrow functions when lexical `this` and concise syntax improve clarity. +- Use `async/await` for async flows and handle failures with `try/catch`. +- Fail safely: validate inputs, handle exceptions, and log actionable error details. +- Use array helpers (`map`, `filter`, `reduce`, `forEach`) where they improve readability. +- Minimize direct DOM writes; batch updates, cache selectors, and use delegation when possible. +- Keep formatting and naming conventions consistent across the project. +- Document non-obvious decisions briefly; avoid redundant comments that restate code. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8cd361b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,1331 @@ +# AGENT.md — PHP Coding Standard + +This file defines the coding standards and working rules for AI agents and developers contributing to this PHP codebase. It is based on the principles from **PHP: The Right Way** and adapted into practical project instructions. + +Source reference: https://phptherightway.com/ + +--- + +## 1. Core Philosophy + +Write PHP that is: + +- **Readable** before clever. +- **Secure by default**. +- **Consistent with community standards**. +- **Easy to test, debug, and refactor**. +- **Separated by responsibility**: routing, controllers, services, models, persistence, templates, and configuration should not be mixed together. + +PHP does not have only one canonical “right way,” so prefer widely accepted standards, documented project conventions, and clear tradeoffs over personal style. + +--- + +## 2. PHP Version Standard + +Use the current stable PHP version supported by the project. + +Default expectation: + +```text +PHP 8.x+ +``` + +Do not introduce code that depends on unsupported PHP versions unless the project explicitly targets a legacy runtime. + +When adding a language feature, verify that it is supported by the project’s configured PHP version in `composer.json`. + +Example: + +```json +{ + "require": { + "php": ">=8.2" + } +} +``` + +--- + +## 3. Coding Style + +Follow recognized PHP standards unless the repository already defines stricter rules. + +Preferred standards: + +- **PSR-1**: Basic Coding Standard +- **PSR-12**: Extended Coding Style +- **PSR-4**: Autoloading + +Use automated tooling rather than manual formatting arguments. + +Recommended tools: + +```bash +composer require --dev squizlabs/php_codesniffer +composer require --dev friendsofphp/php-cs-fixer +``` + +Example checks: + +```bash +vendor/bin/phpcs --standard=PSR12 src tests +vendor/bin/php-cs-fixer fix --dry-run --diff +``` + +Example fix: + +```bash +vendor/bin/php-cs-fixer fix +``` + +### Naming Rules + +Use English names for code symbols and infrastructure. + +Use: + +```php +class InvoiceRepository +{ + public function findByCustomerId(int $customerId): array + { + // ... + } +} +``` + +Avoid unclear abbreviations: + +```php +class InvRepo +{ + public function fbcid($cid) + { + // ... + } +} +``` + +### Formatting Rules + +- Use `value; + } +} +``` + +Guidelines: + +- Keep controllers thin. +- Put business rules in services or domain objects. +- Put persistence logic in repositories or data access classes. +- Use interfaces when multiple implementations are expected or when it improves testing. +- Avoid huge “utility” classes. +- Avoid magic methods unless they provide clear framework integration or a documented benefit. + +--- + +## 8. Dependency Injection + +Prefer dependency injection over creating dependencies inside classes. + +Good: + +```php +final class RegisterUser +{ + public function __construct( + private UserRepository $users, + private PasswordHasher $passwords + ) { + } + + public function handle(string $email, string $plainPassword): void + { + $hash = $this->passwords->hash($plainPassword); + $this->users->create($email, $hash); + } +} +``` + +Avoid: + +```php +final class RegisterUser +{ + public function handle(string $email, string $plainPassword): void + { + $users = new UserRepository(); + $passwords = new PasswordHasher(); + // ... + } +} +``` + +Rules: + +- Constructor injection is preferred for required dependencies. +- Do not use service locators casually. +- Do not hide dependencies in global variables. +- Keep dependency containers at application boundaries, not inside domain logic. + +--- + +## 9. Database Access + +Use PDO or a well-maintained database abstraction layer/ORM. + +Never concatenate untrusted input into SQL. + +Bad: + +```php +$sql = "SELECT * FROM users WHERE id = " . $_GET['id']; +``` + +Good: + +```php +$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); +$stmt->bindValue(':id', $id, PDO::PARAM_INT); +$stmt->execute(); +$user = $stmt->fetch(PDO::FETCH_ASSOC); +``` + +Rules: + +- Use prepared statements and bound parameters. +- Validate input before using it in writes. +- Keep SQL out of templates. +- Keep database access out of controllers where practical. +- Use transactions when multiple writes must succeed or fail together. +- Do not rely only on client-side validation. +- Do not expose raw database errors to users. + +Transaction example: + +```php +$pdo->beginTransaction(); + +try { + $orders->create($order); + $auditLog->record('order.created', $order->id()); + $pdo->commit(); +} catch (Throwable $e) { + $pdo->rollBack(); + throw $e; +} +``` + +--- + +## 10. Input Validation and Output Escaping + +Treat all external data as untrusted. + +Untrusted data includes: + +- `$_GET` +- `$_POST` +- `$_REQUEST` +- `$_COOKIE` +- `$_SERVER` +- uploaded files +- request bodies +- session values +- database values originally supplied by users +- third-party API responses + +### Validate on Input + +Example: + +```php +$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); + +if ($email === false || $email === null) { + throw new InvalidArgumentException('A valid email address is required.'); +} +``` + +### Escape on Output + +For HTML output: + +```php +function e(string $value): string +{ + return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} +``` + +Usage: + +```php +

name()) ?>

+``` + +Rules: + +- Escape based on context: HTML, attribute, JavaScript, CSS, URL, SQL, shell. +- Do not use the same escaping function for every context. +- Prefer template engines with automatic escaping when appropriate. +- Avoid allowing raw HTML from users. If required, sanitize with a proven whitelist sanitizer. +- Use `escapeshellarg()` when passing controlled values to shell commands, and avoid shell execution when possible. +- Never trust file paths supplied by users. Reject path traversal values such as `../`, `/`, `\`, and null bytes. + +--- + +## 11. Passwords and Authentication + +Never store plain-text passwords. + +Use PHP’s password API: + +```php +$hash = password_hash($plainPassword, PASSWORD_DEFAULT); + +if (! password_verify($plainPassword, $hash)) { + throw new RuntimeException('Invalid credentials.'); +} +``` + +Rules: + +- Use `password_hash()` for new password hashes. +- Use `password_verify()` for login checks. +- Use `password_needs_rehash()` when algorithm/cost settings change. +- Do not create your own password hashing algorithm. +- Do not use general-purpose hashes like `md5`, `sha1`, or raw `sha256` for passwords. +- Rate-limit login attempts. +- Regenerate session IDs after login. +- Use secure, HTTP-only, SameSite cookies for sessions. + +--- + +## 12. Serialization and Data Exchange + +Do not call `unserialize()` on untrusted data. + +Prefer JSON for data exchange: + +```php +$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR); +$json = json_encode($data, JSON_THROW_ON_ERROR); +``` + +Rules: + +- Use `JSON_THROW_ON_ERROR` for new code. +- Validate decoded data before using it. +- Avoid PHP serialization for data that crosses trust boundaries. + +--- + +## 13. Configuration and Secrets + +Rules: + +- Keep secrets out of source control. +- Do not commit passwords, API keys, private keys, tokens, or production DSNs. +- Store configuration outside the public web root. +- Use environment variables or ignored local config files for secrets. +- Provide a safe example file such as `.env.example`. + +Example `.gitignore` entries: + +```text +.env +.env.local +/config/local.php +/var/cache/ +/var/log/ +/vendor/ +``` + +Example `.env.example`: + +```text +APP_ENV=local +APP_DEBUG=true +DATABASE_URL=mysql://user:password@localhost:3306/app +``` + +--- + +## 14. Error Handling and Logging + +Use exceptions for exceptional failure paths. + +Development: + +- Show errors locally. +- Log errors. +- Use Xdebug when debugging complex issues. + +Production: + +- Do not display errors to users. +- Log errors to a secure log destination. +- Return safe, generic error messages. +- Preserve enough context in logs for troubleshooting. + +Do not leak: + +- stack traces to users +- SQL statements with secrets +- environment variables +- full filesystem paths +- tokens or passwords + +Example: + +```php +try { + $service->handle($request); +} catch (Throwable $e) { + $logger->error('Order processing failed.', [ + 'exception' => $e, + 'requestId' => $requestId, + ]); + + http_response_code(500); + echo 'An unexpected error occurred.'; +} +``` + +--- + +## 15. Templates and Views + +Keep presentation separate from business logic. + +Rules: + +- Do not query the database from templates. +- Do not place business rules in templates. +- Escape output by default. +- Prefer simple view models or arrays passed into templates. +- Use a template engine with automatic escaping when it fits the project. + +Plain PHP template example: + +```php +

+ + +``` + +--- + +## 16. HTTP and Web Application Rules + +Rules: + +- Use the front controller pattern where appropriate. +- Keep routing separate from business logic. +- Validate request methods. +- Use CSRF protection for state-changing forms. +- Use proper HTTP status codes. +- Redirect after successful POST to avoid duplicate form submission. +- Do not trust headers such as `X-Forwarded-For` unless configured behind a trusted proxy. + +Example POST guard: + +```php +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + exit('Method Not Allowed'); +} +``` + +--- + +## 17. Security Checklist + +Before completing any feature, verify: + +- [ ] All external input is validated. +- [ ] All output is escaped for the correct context. +- [ ] SQL uses prepared statements or safe query builders. +- [ ] Authentication and authorization are checked server-side. +- [ ] Secrets are not committed. +- [ ] Errors are not exposed in production responses. +- [ ] File uploads validate size, extension, MIME type, and storage path. +- [ ] Passwords use `password_hash()` and `password_verify()`. +- [ ] CSRF protection exists for state-changing requests. +- [ ] Dangerous functions are avoided or justified: `eval`, `exec`, `shell_exec`, `system`, `passthru`, `unserialize`. +- [ ] Dependencies have no known vulnerabilities according to `composer audit`. + +--- + +## 18. Testing Standard + +Automated tests are expected for new behavior. + +Preferred tools: + +- PHPUnit +- Pest, if the project already uses it + +Rules: + +- Add or update tests with every behavior change. +- Cover success paths and failure paths. +- Unit-test business logic. +- Integration-test database and framework wiring where useful. +- Functional-test important user flows. +- Avoid relying on `var_dump()` or manual browser testing as the only verification. + +Example PHPUnit test: + +```php +final class InvoiceCalculatorTest extends TestCase +{ + public function testItCalculatesTotalInCents(): void + { + $calculator = new InvoiceCalculator(); + + $total = $calculator->calculateTotal([ + ['amountCents' => 1000], + ['amountCents' => 2500], + ]); + + self::assertSame(3500, $total); + } +} +``` + +Run tests: + +```bash +vendor/bin/phpunit +``` + +--- + +## 19. Static Analysis and Quality Gates + +Use static analysis when available. + +Recommended tools: + +```bash +composer require --dev phpstan/phpstan +composer require --dev vimeo/psalm +``` + +Common quality commands: + +```bash +composer validate +composer audit +vendor/bin/phpcs --standard=PSR12 src tests +vendor/bin/phpunit +vendor/bin/phpstan analyse src tests +``` + +Do not ignore tool failures without documenting why. + +--- + +## 20. Documentation + +Use PHPDoc where it adds clarity, especially for arrays, generics-like structures, complex return values, and public APIs. + +Good: + +```php +/** + * @return list + */ +public function findActiveCustomers(): array +{ + // ... +} +``` + +Avoid noisy comments that repeat the code: + +```php +// Increment i by one. +$i++; +``` + +Rules: + +- Explain why, not just what. +- Document non-obvious tradeoffs. +- Keep README setup instructions current. +- Update examples when behavior changes. + +--- + +## 21. Performance and Caching + +Rules: + +- Measure before optimizing. +- Avoid unnecessary database queries in loops. +- Use pagination for large result sets. +- Cache expensive reads where appropriate. +- Use OPcache in production. +- Do not cache user-specific sensitive data in shared caches without a clear key strategy. + +--- + +## 22. Agent Workflow + +When modifying this codebase, the AI agent must: + +1. Inspect existing project conventions before adding new patterns. +2. Prefer small, focused changes. +3. Preserve public behavior unless explicitly asked to change it. +4. Add or update tests when behavior changes. +5. Run relevant checks when possible. +6. Explain any checks that could not be run. +7. Avoid introducing new dependencies unless they solve a clear problem. +8. Never place secrets in code, tests, fixtures, logs, or documentation. +9. Keep generated code consistent with this file. +10. Leave the repository better organized than it was found. + +--- + +## 23. Pull Request / Review Checklist + +Before considering work complete: + +- [ ] Code follows PSR-12 or project-specific style. +- [ ] Namespaces and autoloading are correct. +- [ ] Composer files are valid. +- [ ] No unrelated dependency updates were introduced. +- [ ] New behavior is tested. +- [ ] Existing tests pass. +- [ ] SQL is parameterized. +- [ ] User input is validated. +- [ ] Output is escaped. +- [ ] No secrets are committed. +- [ ] Errors are handled safely. +- [ ] Documentation was updated where needed. + +--- + +## 24. Legacy PHP Exception Policy + +If this project contains legacy PHP: + +- Do not rewrite large areas without approval. +- Add tests around legacy behavior before refactoring. +- Improve safety incrementally. +- Replace deprecated patterns as touched. +- Avoid mixing modernization with unrelated feature work. +- Document any compatibility constraints. + +Legacy code should still move toward: + +- Composer autoloading +- Namespaces +- PDO/prepared statements +- Centralized configuration +- Automated tests +- Safer error handling + +--- + +## 25. Non-Negotiable Rules + +The agent must not: + +- Commit secrets. +- Build SQL using untrusted string concatenation. +- Store plain-text passwords. +- Use `md5`, `sha1`, or raw fast hashes for passwords. +- Display production errors to users. +- `unserialize()` untrusted data. +- Put database queries in templates. +- Edit files under `vendor/`. +- Add dependencies without a clear reason. +- Ignore failing tests or quality checks without explanation. + +--- + +## 26. Recommended Composer Scripts + +A project may include scripts like this: + +```json +{ + "scripts": { + "test": "phpunit", + "style": "phpcs --standard=PSR12 src tests", + "style:fix": "php-cs-fixer fix", + "analyse": "phpstan analyse src tests", + "quality": [ + "@style", + "@test", + "@analyse" + ] + } +} +``` + +Then run: + +```bash +composer quality +``` + +--- + +## 27. Final Instruction to Coding Agents + +When in doubt, choose the boring, obvious, secure PHP solution: + +- Composer-managed dependencies +- PSR-style code +- Namespaced classes +- Dependency injection +- PDO prepared statements +- Escaped output +- Tested behavior +- Clear errors and logs +- No secrets in source control + + +## Project Overview + +This project is a small PHP MVC framework called MindVisionCode PHP. + +It is intentionally inspired by a Classic ASP MVC framework style: + +- Central dispatcher +- Controllers and actions +- ViewModels +- Repository classes +- Simple validation +- Database migrations +- Small, readable files +- Minimal dependencies + +Do not turn this into Laravel, Symfony, Slim, or another large framework. + +## Tech Stack + +- PHP 8.2+ +- Composer +- PSR-4 autoloading +- PDO +- PHP views +- Optional SQLite/MySQL/SQL Server through PDO + +## Development Commands + +Install dependencies: + +```bash +composer install +``` + +Regenerate autoload files: + +```bash +composer dump-autoload +``` + +Run local server: + +```bash +php -S localhost:8000 -t public +``` + +Run basic tests: + +```bash +php tests/run.php +``` + +## Coding Rules + +- Keep code simple and readable. +- Prefer small classes. +- Use typed properties and return types where practical. +- Avoid hidden magic. +- Do not add dependencies without a clear reason. +- Preserve the framework style. +- Explain any architectural changes. + +## Request Flow + +Browser → public/index.php → Request → Dispatcher → Router → Route → Controller → ViewModel/Repository → View → Response + +## Creating Table +When you create tables and code for a table you will need a corrisponding _audit table with audit_id , id that refrences the object id , an action R I U D , the fields in json , username and created at + +### Color System + +Visual direction: calm operational clarity with high-signal status semantics. + +Core palette: +- Primary: `#1F4E79` (municipal navy; trust and structure) +- Secondary: `#0F766E` (teal support/action) +- Accent: `#2563EB` (interactive focus/action emphasis) + +Semantic status palette: +- Success / On Track: `#2E7D32` +- Warning / At Risk: `#B45309` +- Error / Blocked: `#B91C1C` +- Info / Neutral Progress: `#2563EB` +- Overdue / Critical Flag: `#7F1D1D` + +Neutral foundation: +- Background: `#F7F9FC` +- Surface: `#FFFFFF` +- Border: `#D0D7E2` +- Primary Text: `#111827` +- Secondary Text: `#4B5563` + +Ant Design token strategy: +- Configure global tokens first (`colorPrimary`, `colorSuccess`, `colorWarning`, `colorError`, `colorInfo`, `colorBgBase`, `colorText`, `borderRadius`, density-related sizing). +- Keep status meaning consistent across tables, tags, timelines, and alerts. + +### Typography System + +Tone: professional, clear, non-decorative, optimized for dense operational reading. + +Type stack: +- Primary UI text: `Public Sans`, `Segoe UI`, `Arial`, sans-serif +- Data-heavy/labels fallback: `IBM Plex Sans`, `Segoe UI`, sans-serif +- Monospace for IDs/reference codes: `IBM Plex Mono`, `Consolas`, monospace + +Type hierarchy (desktop-first): +- H1: 28px / 36px / 600 +- H2: 22px / 30px / 600 +- H3: 18px / 26px / 600 +- Body default: 14px / 22px / 400 +- Dense table/body compact: 13px / 20px / 400 +- Caption/meta: 12px / 18px / 400 + +Typography principles: +- Prioritize legibility over brand flourish. +- Keep numeric/date fields highly scannable. +- Use consistent casing and label patterns for form-heavy workflows. + +### Spacing & Layout Foundation + +Layout direction: compact, structured, desktop-optimized operational workspace. + +Spacing system: +- Base unit: 8px +- Micro spacing for dense table controls: 4px +- Section spacing rhythm: 16px / 24px / 32px + +Grid and containers: +- Desktop-first 12-column grid +- Content max-width bands for readability in wide monitors +- Persistent side regions for risk queue/filter panels when useful + +Component density rules: +- Default to compact controls for grid and forms +- Keep touch-target inflation out of scope (PC-only product) +- Reserve larger spacing only for high-risk confirmations/modals + +Visual rhythm: +- Clear row grouping and section boundaries +- Strong alignment around key operational identifiers (municipality, cycle, status, due date) + +### Accessibility Considerations + +- WCAG 2.2 AA minimum contrast targets: + - 4.5:1 for normal text + - 3:1 for large text and UI components +- Keyboard-first operation: + - Visible 2px focus indicators on interactive elements + - Logical tab order in grids, forms, drawers, and modals +- Do not rely on color alone: + - Pair status colors with labels/icons/patterns +- Motion and feedback: + - Subtle motion only for orientation, not decoration + - Respect reduced-motion preferences +- Dense-data readability: + - Preserve minimum font sizes and row heights that remain readable during long operational sessions + +# UI Design +- Refer to template info in the files docs\ux-color-themes.html and docs\ux-design-directions.html + +--- + +## UI/UX Component Rules + +These rules define the established UI patterns for this project. Follow them exactly when building new pages, views, or components. Do not invent new patterns — extend the existing ones. + +--- + +### Navigation + +**Link structure** +Every nav link must be an `` with: +- An inline SVG icon (14×14, `fill="none"`, `stroke="currentColor"`, `stroke-width="1.75"`, Heroicons-style path). +- The label text immediately after the icon. +- `aria-hidden="true"` on the SVG. + +```html + + + Campaigns + +``` + +**Group separators** +Nav links are divided into logical groups separated by ``: +- Group 1: Home +- Group 2: Campaigns, Campaign Types +- Group 3: Jobs, Job Types +- Then a separator before the logged-in user name and logout button. + +Add a separator between groups and before the auth area. Never put a separator at the very start or end of the nav. + +**Active state** +Add `is-active` class to the current page's link. Use exact match for `/`, prefix match (`str_starts_with`) for all others. + +**Logout button** +The logout button must use `class="nav-logout-btn"` — never `button button-secondary`. It is a ghost outlined button styled for the dark header background. Do not use the standard button classes here. + +**Contrast** +Nav link text uses `rgba(255, 255, 255, 0.88)`. Never go below `0.85` — the minimum for WCAG AA on the `#1F4E79` header. + +**Focus rings** +All nav links and the logout button have `outline: 2px solid rgba(255, 255, 255, 0.6)` on `:focus-visible`. Do not remove this. + +--- + +### Page Layout + +**Every page content area** uses a `.content-stack` as its root element. This is a CSS grid with `gap: 24px` that stacks the page toolbar, panels, and other sections vertically. + +```html +
+
...
+
...
+
+``` + +**Page toolbar** (`.page-toolbar`) always contains: +- A `.section-heading` div with an `

` and optional `

` description on the left. +- A primary action button (or back link) on the right. + +```html +

+
+

Page Title

+

One-line description of what this page manages.

+
+ + New Resource +
+``` + +**Page h1 size** is 32px / 40px line-height (set in `.section-heading h1`). Do not override this inline. + +--- + +### Section Panels + +Every content block on a page must be wrapped in `
`. Key rules: + +- The left edge always has `border-left: 3px solid var(--primary)` — this is set globally in CSS and must not be removed or overridden. +- Every panel that has a header must use `.panel-header`, which automatically adds a `border-bottom` separator below it. +- `.panel-header` contains a left `
` with `

` + optional `

`, and a right side for action buttons. +- Panel `

` is 18px. Do not make it larger than the page `

`. + +```html +
+
+
+

Section Title

+

Brief description of this section's content.

+
+ +
+ + +
+``` + +--- + +### Stat Cards (Dashboard Metrics) + +Use `.stat-card` for any numeric summary. Rules: + +- When a stat card navigates somewhere, use `` — not a `
`. The CSS `a.stat-card` handles hover lift and the "View all →" affordance automatically. +- When a stat card is purely informational (no destination), use `
`. +- Numbers display at 38px / font-weight 700 in `var(--primary)`. +- Labels use the `.stat-card span` style: 11px uppercase, `var(--text-secondary)`. +- Never put more than one number in a stat card. +- Place stat cards in a `.stats-grid` container. For 4 cards use `.stats-grid.stats-grid-4`. For 3 use `.stats-grid` alone (default 3-column). + +```html + +``` + +--- + +### Dashboard Pages + +When a page is an overview/metrics page (like the home dashboard): + +1. Start with a `.stats-grid` of stat cards covering the main record counts. +2. Follow with a `.dashboard-panels` grid of two side-by-side `.section-panel` blocks. +3. Left panel: a recent-records list using `.dashboard-table`. +4. Right panel: a breakdown or summary using `.type-breakdown`. + +**Dashboard table** (`.dashboard-table`) rules: +- Columns: ID (`.dashboard-table-id`), name/type, date (`.dashboard-table-date`), action link (`.dashboard-table-action`). +- No JavaScript — server-rendered, static HTML table. +- Always include a link column to the resource's edit page. +- Always include an empty state (`.empty-state`) with a helpful create link for when there is no data. + +**Type breakdown** (`.type-breakdown`) rules: +- Each row is a `.type-breakdown-row` showing: label, proportional bar, count. +- When the rows link somewhere use ``. The CSS handles hover and focus-visible. +- Calculate the bar width as `round(count / max * 100)%`. Pre-calculate `$max` before the loop. + +--- + +### Forms + +**Structure** +All multi-section forms use `.ct-form` (a CSS grid with `gap: 24px`) containing one or more `.form-section` blocks, ending with `.form-actions`. + +```html +
+ + +
+ +
+ +
+
+``` + +**Sticky save bar** +`.ct-form .form-actions` is `position: sticky; bottom: 0` — the save bar sticks to the viewport bottom when the form is taller than the screen. This is set globally in CSS. Do not override or move form-actions outside `.ct-form`. + +**Native validation** +Use HTML5 `required`, `maxlength`, `pattern`, `min`, `max` on inputs. The CSS rule `.input:invalid:not(:placeholder-shown)` automatically shows an error border without JavaScript. Use `novalidate` on the form tag to prevent browser-native popups while still benefiting from the CSS pseudo-class. + +**Required mark** +Mark required fields with `*` inside the label. Color is `var(--error)` via CSS. + +**Error messages** +Server-side validation errors go in `` immediately after the input. Add `input-error` class to the input when it has a server error. + +--- + +### Attribute Builder (Drag-Reorder Lists) + +When a form includes a reorderable list of items (like campaign type attributes): + +- Each row is `.attribute-row` with `draggable="true"` and Alpine.js drag event handlers. +- The first element of every row must be ``. + The CSS hides the unicode character with `font-size: 0` and replaces it with a `::before` radial-gradient dot grid — this is automatic. Do not change the HTML to use an SVG or other icon. +- `is-dragging` and `is-drag-over` classes are applied by Alpine and styled by CSS — do not add custom drag styles. + +--- + +### Loading States + +**Never use plain text** for loading indicators in content areas. Use `.skeleton-rows` with `.skeleton-row` children. + +```html +
+
+
+
+
+
+
+``` + +Rules: +- Use 5 skeleton rows for full-page table loaders. +- Use 3 skeleton rows for smaller/drilldown table loaders. +- Always pair with `x-cloak` and `x-show` on Alpine-controlled sections so the skeleton only shows while loading. +- The shimmer animation and fade-out opacity are set automatically by CSS via `:nth-child` selectors. + +--- + +### Interactive Link Patterns + +**Cards, rows, and panels that navigate** must be `` elements, not `
` elements with `onclick`. This applies to: +- Stat cards with a destination +- Type breakdown rows +- Any list row that is entirely clickable + +Use the appropriate CSS modifier class (`a.stat-card`, `a.type-breakdown-row`) — the hover and focus styles are already defined. + +**Inline table action links** (Edit, Delete) in Tabulator formatter functions must use these conventions: +- Edit: `class="button button-secondary button-sm"` +- Delete: `class="button button-danger button-sm"` with a `window.confirmXxx()` guard function +- Jobs/drill-down: `class="button button-primary button-sm"` + +--- + +### Accessibility + +**Focus rings** +- All content-area links get `outline: 2px solid var(--accent); outline-offset: 2px` on `:focus-visible` — set globally in CSS via `.page-content a:focus-visible`. Do not suppress this. +- All buttons already have `outline: 2px solid var(--accent); outline-offset: 2px` via `.button:focus-visible`. +- Nav links and logout button have white outline rings — see the Navigation section above. + +**Contrast minimums** +- Normal body text: minimum 4.5:1 against its background. +- Nav link text: minimum `rgba(255,255,255, 0.85)` against `#1F4E79`. +- Secondary text (`var(--text-secondary): #4B5563`) on white: ~7.2:1 — acceptable. +- Do not use `var(--text-muted)` (`#6B7280`) for body text — only for supplementary labels and counts. + +**ARIA** +- All SVG icons in nav links must have `aria-hidden="true"`. +- All `.nav-sep` elements must have `aria-hidden="true"`. +- Empty states must be descriptive — include both what is missing and a create action link. + +--- + +### Repository Patterns for Metrics + +When a page needs aggregate data (counts, recent records, breakdowns), add methods to the relevant repository. Do not inline aggregate SQL in the controller. + +**Count method pattern** +```php +public function count(): int +{ + $row = $this->database->first('SELECT COUNT(*) AS total FROM table_name'); + return (int) ($row['total'] ?? 0); +} +``` + +**Recent records pattern** +Inline the `TOP` limit directly — SQL Server does not accept bound parameters in `TOP` expressions. +```php +public function recentWithType(int $limit = 5): array +{ + return $this->database->query( + "SELECT TOP ({$limit}) t.id, t.created_at, ref.name AS type_name + FROM main_table t + INNER JOIN ref_table ref ON t.ref_id = ref.id + ORDER BY t.id DESC" + ); +} +``` + +**Grouped count pattern** +Use `LEFT JOIN` so that reference rows with zero records still appear. +```php +public function countByType(): array +{ + return $this->database->query( + 'SELECT ref.name AS type_name, COUNT(t.id) AS record_count + FROM ref_table ref + LEFT JOIN main_table t ON t.ref_id = ref.id + GROUP BY ref.id, ref.name + ORDER BY record_count DESC, ref.name ASC' + ); +} +``` + +--- + +### ViewModels for Dashboard Pages + +Dashboard pages use a dedicated ViewModel. Metric fields use typed defaults so the view is never working with `null`. + +```php +class MyDashboardViewModel +{ + public int $totalThings = 0; + public int $totalTypes = 0; + public array $recentItems = []; + public array $itemsByType = []; +} +``` + +Populate it in the controller using private `repo()` methods or inline `new Repository(database())` calls — consistent with how other controllers are written in this project. + +--- + +## Clean JavaScript Practices (Distilled) + +Source: https://medium.com/@onix_react/best-practices-for-writing-clean-javascript-code-a4e5755de69a + +- Prefer `const` and `let` over `var` to avoid function-scoped surprises. +- Keep scope tight and avoid globals to prevent hidden coupling and collisions. +- Use small, focused functions and clear names to make intent obvious. +- Prefer arrow functions when lexical `this` and concise syntax improve clarity. +- Use `async/await` for async flows and handle failures with `try/catch`. +- Fail safely: validate inputs, handle exceptions, and log actionable error details. +- Use array helpers (`map`, `filter`, `reduce`, `forEach`) where they improve readability. +- Minimize direct DOM writes; batch updates, cache selectors, and use delegation when possible. +- Keep formatting and naming conventions consistent across the project. +- Document non-obvious decisions briefly; avoid redundant comments that restate code. \ No newline at end of file diff --git a/app/Controllers/ApiProxyController.php b/app/Controllers/ApiProxyController.php new file mode 100644 index 0000000..4fed8c9 --- /dev/null +++ b/app/Controllers/ApiProxyController.php @@ -0,0 +1,48 @@ +input('url') ?? '')); + + if ($url === '' || + !filter_var($url, FILTER_VALIDATE_URL) || + !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true) + ) { + return Response::json(['error' => 'Invalid or missing URL.'], 400); + } + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_USERAGENT => 'CampaignTracker-ApiProxy/1.0', + CURLOPT_HTTPHEADER => ['Accept: application/json, application/xml, text/xml, text/plain, */*'], + ]); + + $body = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_error($ch); + curl_close($ch); + + if ($body === false) { + return Response::json(['error' => $curlErr ?: 'Outbound request failed.'], 502); + } + + return Response::json(['body' => (string) $body, 'http_status' => $httpCode]); + } +} diff --git a/app/Controllers/JobTypeController.php b/app/Controllers/JobTypeController.php index 0767d4e..2b734dd 100644 --- a/app/Controllers/JobTypeController.php +++ b/app/Controllers/JobTypeController.php @@ -218,19 +218,42 @@ class JobTypeController extends Controller ->maxLength('name', $name, 255, 'Name must be 255 characters or fewer.') ->errors()); + $attributeAliases = (array) ($request->input('attribute_alias') ?? []); + $attributeApiUrls = (array) ($request->input('attribute_api_url') ?? []); + $attributeApiFormats = (array) ($request->input('attribute_api_format') ?? []); + $attributeApiReturnTypes = (array) ($request->input('attribute_api_return_type') ?? []); + $attributeApiMatchFields = (array) ($request->input('attribute_api_match_field') ?? []); + $attributeApiAutoFills = (array) ($request->input('attribute_api_auto_fill') ?? []); + $attributes = []; foreach ($attributeNames as $i => $attrName) { $attrName = trim((string) $attrName); $attrType = trim((string) ($attributeTypes[$i] ?? 'text')); if ($attrName === '') continue; - $attributes[] = [ + + $validatedType = in_array($attrType, ['text', 'number', 'date', 'boolean', 'api_lookup'], true) ? $attrType : 'text'; + + $attr = [ 'name' => $attrName, - 'type' => in_array($attrType, ['text', 'number', 'date', 'boolean'], true) ? $attrType : 'text', + 'type' => $validatedType, + 'alias' => trim((string) ($attributeAliases[$i] ?? '')), 'order' => isset($attributeOrders[$i]) && (string) $attributeOrders[$i] !== '' ? max(1, (int) $attributeOrders[$i]) : count($attributes) + 1, ]; + + if ($validatedType === 'api_lookup') { + $rawFormat = trim((string) ($attributeApiFormats[$i] ?? '')); + $rawReturnType = trim((string) ($attributeApiReturnTypes[$i] ?? '')); + $attr['api_url'] = trim((string) ($attributeApiUrls[$i] ?? '')); + $attr['api_format'] = in_array($rawFormat, ['json', 'xml'], true) ? $rawFormat : 'json'; + $attr['api_return_type'] = in_array($rawReturnType, ['text', 'number', 'date', 'boolean'], true) ? $rawReturnType : 'text'; + $attr['api_match_field'] = trim((string) ($attributeApiMatchFields[$i] ?? '')); + $attr['api_auto_fill'] = trim((string) ($attributeApiAutoFills[$i] ?? '')); + } + + $attributes[] = $attr; } usort($attributes, static fn(array $a, array $b): int => $a['order'] <=> $b['order']); diff --git a/app/Views/job-types/create.php b/app/Views/job-types/create.php index 6def950..945a73d 100644 --- a/app/Views/job-types/create.php +++ b/app/Views/job-types/create.php @@ -45,30 +45,77 @@ x-on:drop="drop($event, index)" x-on:dragend="dragEnd()" :class="{ 'is-dragging': dragIndex === index, 'is-drag-over': dragOverIndex === index && dragIndex !== index }"> - - - - -
- +
+ + + + + +
+ +
+
+
+ + + + +
diff --git a/app/Views/job-types/edit.php b/app/Views/job-types/edit.php index 3dc7a73..fbe7568 100644 --- a/app/Views/job-types/edit.php +++ b/app/Views/job-types/edit.php @@ -52,30 +52,77 @@ x-on:drop="drop($event, index)" x-on:dragend="dragEnd()" :class="{ 'is-dragging': dragIndex === index, 'is-drag-over': dragOverIndex === index && dragIndex !== index }"> - - - - -
- +
+ + + + + +
+ +
+
+
+ + + + +
diff --git a/app/Views/jobs/create.php b/app/Views/jobs/create.php index 1f7684f..f814aa1 100644 --- a/app/Views/jobs/create.php +++ b/app/Views/jobs/create.php @@ -76,7 +76,7 @@ window.__initialJtVals = form['attribute_values'], JSON_