feat: ticket contact linking, multi-assignee, and push-based pre-release
Tickets can now link to Joomla contact records (optional contact_id FK). Replaces single assigned_to with junction table for multi-assignee support — tickets can be assigned to multiple users and/or user groups. Updates pre-release workflow to trigger on push to dev/alpha/beta/rc.
This commit is contained in:
@@ -8,19 +8,17 @@
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
@@ -43,12 +41,11 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -56,7 +53,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
@@ -87,9 +84,14 @@ jobs:
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
case "${{ github.ref_name }}" in
|
||||
rc) STABILITY="release-candidate" ;;
|
||||
alpha) STABILITY="alpha" ;;
|
||||
beta) STABILITY="beta" ;;
|
||||
*) STABILITY="development" ;;
|
||||
esac
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
@@ -164,7 +166,7 @@ jobs:
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
|
||||
@@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` (
|
||||
`status` ENUM('open','in_progress','waiting','resolved','closed') NOT NULL DEFAULT 'open',
|
||||
`priority` ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal',
|
||||
`category_id` INT UNSIGNED DEFAULT NULL,
|
||||
`contact_id` INT UNSIGNED DEFAULT NULL,
|
||||
`created_by` INT NOT NULL DEFAULT 0,
|
||||
`assigned_to` INT DEFAULT NULL,
|
||||
`created` DATETIME NOT NULL,
|
||||
@@ -37,6 +38,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` (
|
||||
KEY `idx_priority` (`priority`),
|
||||
KEY `idx_assigned` (`assigned_to`),
|
||||
KEY `idx_category` (`category_id`),
|
||||
KEY `idx_contact` (`contact_id`),
|
||||
KEY `idx_created` (`created`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
@@ -72,6 +74,17 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_automation` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
|
||||
`assignee_id` INT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
|
||||
KEY `idx_ticket` (`ticket_id`),
|
||||
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Default automation rules
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
|
||||
(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Add contact link to tickets (optional FK to #__contact_details)
|
||||
ALTER TABLE `#__mokosuite_tickets`
|
||||
ADD COLUMN `contact_id` INT UNSIGNED DEFAULT NULL AFTER `category_id`,
|
||||
ADD KEY `idx_contact` (`contact_id`);
|
||||
|
||||
-- Multi-assignee junction table (replaces single assigned_to column)
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
|
||||
`assignee_id` INT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
|
||||
KEY `idx_ticket` (`ticket_id`),
|
||||
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrate existing single-assignee data to junction table
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_assignees` (`ticket_id`, `assignee_type`, `assignee_id`)
|
||||
SELECT `id`, 'user', `assigned_to` FROM `#__mokosuite_tickets` WHERE `assigned_to` IS NOT NULL AND `assigned_to` > 0;
|
||||
@@ -30,17 +30,18 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->quoteName('t.priority'),
|
||||
$db->quoteName('t.created'),
|
||||
$db->quoteName('t.modified'),
|
||||
$db->quoteName('t.contact_id'),
|
||||
$db->quoteName('t.sla_response_due'),
|
||||
$db->quoteName('t.sla_resolution_due'),
|
||||
$db->quoteName('t.sla_responded'),
|
||||
$db->quoteName('c.title', 'category_title'),
|
||||
$db->quoteName('u.name', 'created_by_name'),
|
||||
$db->quoteName('a.name', 'assigned_to_name'),
|
||||
$db->quoteName('ct.name', 'contact_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
||||
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to');
|
||||
->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id');
|
||||
|
||||
if (!empty($filters['status']))
|
||||
{
|
||||
@@ -62,12 +63,24 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['contact_id']))
|
||||
{
|
||||
$query->where($db->quoteName('t.contact_id') . ' = ' . (int) $filters['contact_id']);
|
||||
}
|
||||
|
||||
$query->order($db->quoteName('t.created') . ' DESC');
|
||||
$query->setLimit(50);
|
||||
|
||||
$db->setQuery($query);
|
||||
$tickets = $db->loadObjectList() ?: [];
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
// Load assignees for each ticket
|
||||
foreach ($tickets as $ticket)
|
||||
{
|
||||
$ticket->assignees = $this->getTicketAssignees((int) $ticket->id);
|
||||
}
|
||||
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,11 +96,15 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->quoteName('u.name', 'created_by_name'),
|
||||
$db->quoteName('u.email', 'created_by_email'),
|
||||
$db->quoteName('a.name', 'assigned_to_name'),
|
||||
$db->quoteName('ct.name', 'contact_name'),
|
||||
$db->quoteName('ct.email_to', 'contact_email'),
|
||||
$db->quoteName('ct.telephone', 'contact_phone'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
||||
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to')
|
||||
->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id')
|
||||
->where($db->quoteName('t.id') . ' = ' . $id);
|
||||
$db->setQuery($query);
|
||||
$ticket = $db->loadObject();
|
||||
@@ -113,6 +130,9 @@ class TicketsModel extends BaseDatabaseModel
|
||||
// Reply count
|
||||
$ticket->reply_count = \count($ticket->replies);
|
||||
|
||||
// Load assignees (users + groups)
|
||||
$ticket->assignees = $this->getTicketAssignees($id);
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
@@ -133,6 +153,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'status' => 'open',
|
||||
'priority' => $data['priority'] ?? 'normal',
|
||||
'category_id' => (int) ($data['category_id'] ?? 0) ?: null,
|
||||
'contact_id' => (int) ($data['contact_id'] ?? 0) ?: null,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null,
|
||||
'created' => $now,
|
||||
@@ -174,6 +195,21 @@ class TicketsModel extends BaseDatabaseModel
|
||||
|
||||
$db->insertObject('#__mokosuite_tickets', $ticket, 'id');
|
||||
|
||||
// Handle multi-assignee (users and groups)
|
||||
$assignUsers = array_filter(array_map('intval', (array) ($data['assign_users'] ?? [])));
|
||||
$assignGroups = array_filter(array_map('intval', (array) ($data['assign_groups'] ?? [])));
|
||||
|
||||
// Backward compat: single assigned_to becomes a user assignee
|
||||
if (empty($assignUsers) && $ticket->assigned_to)
|
||||
{
|
||||
$assignUsers = [$ticket->assigned_to];
|
||||
}
|
||||
|
||||
if (!empty($assignUsers) || !empty($assignGroups))
|
||||
{
|
||||
$this->setTicketAssignees((int) $ticket->id, $assignUsers, $assignGroups);
|
||||
}
|
||||
|
||||
// Run automation + notifications
|
||||
$this->runAutomation('ticket_created', (int) $ticket->id);
|
||||
NotificationService::notify('ticket_created', $this->getTicket((int) $ticket->id));
|
||||
@@ -317,6 +353,113 @@ class TicketsModel extends BaseDatabaseModel
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignees for a ticket (users and groups with resolved names).
|
||||
*/
|
||||
public function getTicketAssignees(int $ticketId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_assignees'))
|
||||
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
|
||||
);
|
||||
$rows = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
if ($row->assignee_type === 'user')
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('name'))
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
|
||||
);
|
||||
$row->name = (string) $db->loadResult() ?: 'User #' . $row->assignee_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('title'))
|
||||
->from($db->quoteName('#__usergroups'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
|
||||
);
|
||||
$row->name = (string) $db->loadResult() ?: 'Group #' . $row->assignee_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set assignees for a ticket (replaces existing assignments).
|
||||
*
|
||||
* @param int $ticketId Ticket ID
|
||||
* @param array $users Array of user IDs
|
||||
* @param array $groups Array of user group IDs
|
||||
*/
|
||||
public function setTicketAssignees(int $ticketId, array $users = [], array $groups = []): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Clear existing
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_ticket_assignees'))
|
||||
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
|
||||
// Insert users
|
||||
foreach ($users as $uid)
|
||||
{
|
||||
$uid = (int) $uid;
|
||||
|
||||
if ($uid > 0)
|
||||
{
|
||||
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'assignee_type' => 'user',
|
||||
'assignee_id' => $uid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert groups
|
||||
foreach ($groups as $gid)
|
||||
{
|
||||
$gid = (int) $gid;
|
||||
|
||||
if ($gid > 0)
|
||||
{
|
||||
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'assignee_type' => 'group',
|
||||
'assignee_id' => $gid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published Joomla contact records for ticket linking.
|
||||
*/
|
||||
public function getContacts(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('name')])
|
||||
->from($db->quoteName('#__contact_details'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('name') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canned responses, optionally filtered by category.
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@ class HtmlView extends BaseHtmlView
|
||||
protected $statusCounts;
|
||||
protected $overdue = [];
|
||||
protected $atsAvailable = null;
|
||||
protected $contacts = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
@@ -32,6 +33,7 @@ class HtmlView extends BaseHtmlView
|
||||
'status' => $app->getInput()->getString('filter_status', ''),
|
||||
'priority' => $app->getInput()->getString('filter_priority', ''),
|
||||
'category_id' => $app->getInput()->getInt('filter_category', 0),
|
||||
'contact_id' => $app->getInput()->getInt('filter_contact', 0),
|
||||
];
|
||||
|
||||
$this->tickets = $model->getTickets($filters);
|
||||
@@ -39,6 +41,7 @@ class HtmlView extends BaseHtmlView
|
||||
$this->statusCounts = $model->getStatusCounts();
|
||||
$this->overdue = $model->getOverdueTickets();
|
||||
$this->atsAvailable = $model->checkAtsAvailable();
|
||||
$this->contacts = $model->getContacts();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
|
||||
@@ -90,7 +90,25 @@ $priorityBadge = [
|
||||
<tr><td class="text-muted">Priority</td><td><span class="badge <?php echo $priorityBadge[$t->priority] ?? ''; ?>"><?php echo ucfirst($t->priority); ?></span></td></tr>
|
||||
<tr><td class="text-muted">Category</td><td><?php echo $this->escape($t->category_title ?? '—'); ?></td></tr>
|
||||
<tr><td class="text-muted">Created By</td><td><?php echo $this->escape($t->created_by_name); ?><br><small><?php echo $this->escape($t->created_by_email ?? ''); ?></small></td></tr>
|
||||
<tr><td class="text-muted">Assigned To</td><td><?php echo $this->escape($t->assigned_to_name ?? 'Unassigned'); ?></td></tr>
|
||||
<tr><td class="text-muted">Assigned To</td><td><?php
|
||||
if (!empty($t->assignees)) {
|
||||
foreach ($t->assignees as $a) {
|
||||
$icon = $a->assignee_type === 'group' ? '<span class="icon-users"></span> ' : '<span class="icon-user"></span> ';
|
||||
echo '<div>' . $icon . $this->escape($a->name) . '</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<em>Unassigned</em>';
|
||||
}
|
||||
?></td></tr>
|
||||
<?php if ($t->contact_id): ?>
|
||||
<tr><td class="text-muted">Contact</td><td>
|
||||
<a href="<?php echo Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $t->contact_id); ?>">
|
||||
<?php echo $this->escape($t->contact_name ?? 'Contact #' . $t->contact_id); ?>
|
||||
</a>
|
||||
<?php if (!empty($t->contact_email)): ?><br><small><?php echo $this->escape($t->contact_email); ?></small><?php endif; ?>
|
||||
<?php if (!empty($t->contact_phone)): ?><br><small><?php echo $this->escape($t->contact_phone); ?></small><?php endif; ?>
|
||||
</td></tr>
|
||||
<?php endif; ?>
|
||||
<tr><td class="text-muted">Created</td><td><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></td></tr>
|
||||
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
|
||||
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
|
||||
|
||||
@@ -88,6 +88,7 @@ $priorityBadge = [
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Category</th>
|
||||
<th>Contact</th>
|
||||
<th>Created By</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Created</th>
|
||||
@@ -96,7 +97,7 @@ $priorityBadge = [
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($tickets)): ?>
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">No tickets found.</td></tr>
|
||||
<tr><td colspan="10" class="text-center text-muted py-4">No tickets found.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($tickets as $t): ?>
|
||||
<?php
|
||||
@@ -112,8 +113,20 @@ $priorityBadge = [
|
||||
<td><span class="badge <?php echo $statusBadge[$t->status] ?? 'bg-secondary'; ?>"><?php echo ucwords(str_replace('_', ' ', $t->status)); ?></span></td>
|
||||
<td><span class="badge <?php echo $priorityBadge[$t->priority] ?? 'bg-secondary'; ?>"><?php echo ucfirst($t->priority); ?></span></td>
|
||||
<td><?php echo $this->escape($t->category_title ?? '—'); ?></td>
|
||||
<td><?php echo $t->contact_name ? '<a href="' . Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $t->contact_id) . '">' . $this->escape($t->contact_name) . '</a>' : '—'; ?></td>
|
||||
<td><?php echo $this->escape($t->created_by_name ?? ''); ?></td>
|
||||
<td><?php echo $t->assigned_to_name ? $this->escape($t->assigned_to_name) : '<em>Unassigned</em>'; ?></td>
|
||||
<td><?php
|
||||
if (!empty($t->assignees)) {
|
||||
$names = [];
|
||||
foreach ($t->assignees as $a) {
|
||||
$icon = $a->assignee_type === 'group' ? '<span class="icon-users"></span> ' : '';
|
||||
$names[] = $icon . $this->escape($a->name);
|
||||
}
|
||||
echo implode(', ', $names);
|
||||
} else {
|
||||
echo '<em>Unassigned</em>';
|
||||
}
|
||||
?></td>
|
||||
<td class="small"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></td>
|
||||
<td class="small">
|
||||
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
|
||||
@@ -157,7 +170,7 @@ $priorityBadge = [
|
||||
<input type="text" name="subject" id="modal-subject" class="form-control" required>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category_id" class="form-select">
|
||||
<option value="">— Select —</option>
|
||||
@@ -166,7 +179,7 @@ $priorityBadge = [
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="normal">Normal</option>
|
||||
@@ -175,6 +188,15 @@ $priorityBadge = [
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Contact</label>
|
||||
<select name="contact_id" class="form-select">
|
||||
<option value="">— None —</option>
|
||||
<?php foreach ($this->contacts as $contact): ?>
|
||||
<option value="<?php echo $contact->id; ?>"><?php echo $this->escape($contact->name); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
|
||||
Reference in New Issue
Block a user