diff --git a/.env.example b/.env.example
index b9c24e4..5d02e12 100644
--- a/.env.example
+++ b/.env.example
@@ -10,5 +10,13 @@ DB_NAME=ticketing_system
# Discord Webhook (optional - for notifications)
DISCORD_WEBHOOK_URL=
+# Application Domain (required for Discord webhook links)
+# Set this to your public domain (e.g., t.lotusguild.org)
+APP_DOMAIN=
+
+# Allowed Hosts for HTTP_HOST validation (comma-separated)
+# Include all domains that can access this application
+ALLOWED_HOSTS=localhost,127.0.0.1
+
# Timezone (default: America/New_York)
TIMEZONE=America/New_York
diff --git a/Claude.md b/Claude.md
index 120d324..db31fc4 100644
--- a/Claude.md
+++ b/Claude.md
@@ -241,6 +241,7 @@ All admin pages are accessible via the **Admin dropdown** in the dashboard heade
10. **API routing**: All API endpoints must be added to `index.php` router
11. **Session in APIs**: RateLimitMiddleware starts session; don't call session_start() again
12. **Database collation**: Use `utf8mb4_general_ci` (not unicode_ci) for new tables
+13. **Discord webhook URLs**: Set `APP_DOMAIN` in `.env` for correct ticket URLs in Discord notifications
13. **Ticket ID extraction**: Use `getTicketIdFromUrl()` helper in JS files
14. **CSP Nonces**: All inline scripts require `nonce=""` attribute
15. **Visibility validation**: Internal visibility requires groups; code validates this
diff --git a/README.md b/README.md
index 55f9105..f4b0d0f 100644
--- a/README.md
+++ b/README.md
@@ -187,9 +187,12 @@ DB_USER=tinkertickets
DB_PASS=your_password
DB_NAME=ticketing_system
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
+APP_DOMAIN=t.lotusguild.org
TIMEZONE=America/New_York
```
+**Note**: `APP_DOMAIN` is required for Discord webhook ticket links to work correctly. Without it, links will default to localhost.
+
### 2. Cron Jobs
Add to crontab for recurring tickets:
diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js
index 669eb08..dcbaf64 100644
--- a/assets/js/dashboard.js
+++ b/assets/js/dashboard.js
@@ -643,6 +643,10 @@ function loadTemplate() {
document.getElementById('priority').value = '4';
document.getElementById('category').value = 'General';
document.getElementById('type').value = 'Issue';
+ const assignedToSelect = document.getElementById('assigned_to');
+ if (assignedToSelect) {
+ assignedToSelect.value = '';
+ }
return;
}
diff --git a/controllers/TicketController.php b/controllers/TicketController.php
index 904aa3b..90c5626 100644
--- a/controllers/TicketController.php
+++ b/controllers/TicketController.php
@@ -108,13 +108,15 @@ class TicketController {
'category' => $_POST['category'] ?? 'General',
'type' => $_POST['type'] ?? 'Issue',
'visibility' => $_POST['visibility'] ?? 'public',
- 'visibility_groups' => $visibilityGroups
+ 'visibility_groups' => $visibilityGroups,
+ 'assigned_to' => !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null
];
// Validate input
if (empty($ticketData['title'])) {
$error = "Title is required";
$templates = $this->templateModel->getAllTemplates();
+ $allUsers = $this->userModel->getAllUsers();
$conn = $this->conn; // Make $conn available to view
include dirname(__DIR__) . '/views/CreateTicketView.php';
return;
@@ -138,6 +140,7 @@ class TicketController {
} else {
$error = $result['error'];
$templates = $this->templateModel->getAllTemplates();
+ $allUsers = $this->userModel->getAllUsers();
$conn = $this->conn; // Make $conn available to view
include dirname(__DIR__) . '/views/CreateTicketView.php';
return;
@@ -145,6 +148,8 @@ class TicketController {
} else {
// Get all templates for the template selector
$templates = $this->templateModel->getAllTemplates();
+ // Get all users for assignment dropdown
+ $allUsers = $this->userModel->getAllUsers();
$conn = $this->conn; // Make $conn available to view
// Display the create ticket form
diff --git a/models/TicketModel.php b/models/TicketModel.php
index 70a55c5..7935ed5 100644
--- a/models/TicketModel.php
+++ b/models/TicketModel.php
@@ -376,8 +376,8 @@ class TicketModel {
error_log("Ticket ID generation used fallback after {$maxAttempts} random attempts");
}
- $sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, visibility, visibility_groups)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ $sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, assigned_to, visibility, visibility_groups)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->conn->prepare($sql);
@@ -388,6 +388,7 @@ class TicketModel {
$type = $ticketData['type'] ?? 'Issue';
$visibility = $ticketData['visibility'] ?? 'public';
$visibilityGroups = $ticketData['visibility_groups'] ?? null;
+ $assignedTo = !empty($ticketData['assigned_to']) ? (int)$ticketData['assigned_to'] : null;
// Validate visibility
$allowedVisibilities = ['public', 'internal', 'confidential'];
@@ -409,7 +410,7 @@ class TicketModel {
}
$stmt->bind_param(
- "sssssssiss",
+ "sssssssiiss",
$ticket_id,
$ticketData['title'],
$ticketData['description'],
@@ -418,6 +419,7 @@ class TicketModel {
$category,
$type,
$createdBy,
+ $assignedTo,
$visibility,
$visibilityGroups
);
diff --git a/views/CreateTicketView.php b/views/CreateTicketView.php
index e8bac56..41690e2 100644
--- a/views/CreateTicketView.php
+++ b/views/CreateTicketView.php
@@ -13,7 +13,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
-
+
-
+
-
+