Add assignment dropdown on ticket creation and fix Discord webhook URLs
- Add APP_DOMAIN config for correct Discord webhook ticket links - Add "Assign To" dropdown on create ticket form - Update TicketModel.createTicket() to support assigned_to field - Update documentation for APP_DOMAIN requirement Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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="<?php echo $nonce; ?>"` attribute
|
||||
15. **Visibility validation**: Internal visibility requires groups; code validates this
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
|
||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// CSRF Token for AJAX requests
|
||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||
@@ -166,6 +166,32 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 4b: Assignment -->
|
||||
<div class="ascii-section-header">Assignment</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="detail-group">
|
||||
<label for="assigned_to">Assign To (Optional)</label>
|
||||
<select id="assigned_to" name="assigned_to" class="editable">
|
||||
<option value="">-- Unassigned --</option>
|
||||
<?php if (isset($allUsers) && !empty($allUsers)): ?>
|
||||
<?php foreach ($allUsers as $user): ?>
|
||||
<option value="<?php echo $user['user_id']; ?>">
|
||||
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p style="color: var(--terminal-green); font-size: 0.85rem; margin-top: 0.5rem; font-family: var(--font-mono);">
|
||||
Select a user to assign this ticket to
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<!-- SECTION 5: Visibility Settings -->
|
||||
<div class="ascii-section-header">Visibility Settings</div>
|
||||
<div class="ascii-content">
|
||||
|
||||
@@ -16,7 +16,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260131e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// CSRF Token for AJAX requests
|
||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||
|
||||
@@ -54,7 +54,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260131e">
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260131e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js?v=20260131e"></script>
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// CSRF Token for AJAX requests
|
||||
|
||||
Reference in New Issue
Block a user