Add application support teams with contacts
All checks were successful
CI / backend (push) Successful in 1m7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

Replaces the legacy supportteams/appowners pair: supportteams
(teamname unique, teamurl ServiceNow link) + supportteamcontacts
(multiple named contacts with SSO per team, the people you reach out
to), applications.supportteamid intact. Migration 7d18 migrates each
legacy team owner into a contact, drops appowners, and has a validated
downgrade. New /api/supportteams CRUD (admin writes, import-mode
timestamps, teamname lookup), Support card on application detail,
contacts column on the list, and a settings management page.
IMPORT-API.md mapping updated to the concrete endpoints.

658 tests pass; live dev migration applied (24 teams / 24 contacts);
fresh-install and downgrade round-trips verified on scratch DBs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 20:29:12 -04:00
parent 46e50c07ff
commit 7dae281993
18 changed files with 1095 additions and 172 deletions

View File

@@ -48,26 +48,26 @@
<div class="content-grid">
<!-- Left Column -->
<div class="content-column">
<!-- Support Info -->
<!-- Support -->
<div class="section-card">
<h3 class="section-title">Support Information</h3>
<h3 class="section-title">Support</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Support Team</span>
<span class="info-value">
<a v-if="app.supportteam?.teamurl" :href="app.supportteam.teamurl" target="_blank">
{{ app.supportteam?.teamname || '-' }}
<a v-if="app.teamurl" :href="app.teamurl" target="_blank">
{{ app.supportteamname || '-' }}
</a>
<span v-else>{{ app.supportteam?.teamname || '-' }}</span>
<span v-else>{{ app.supportteamname || '-' }}</span>
</span>
</div>
<div class="info-row">
<span class="info-label">App Owner</span>
<span class="info-value">{{ app.supportteam?.owner?.appowner || '-' }}</span>
</div>
<div class="info-row" v-if="app.supportteam?.owner?.sso">
<span class="info-label">SSO</span>
<span class="info-value mono">{{ app.supportteam.owner.sso }}</span>
<div class="info-row" v-if="app.contacts && app.contacts.length">
<span class="info-label">Contacts</span>
<span class="info-value">
<span v-for="(contact, index) in app.contacts" :key="index" class="contact-line">
{{ contact.name }}<span v-if="contact.sso" class="mono"> ({{ contact.sso }})</span>
</span>
</span>
</div>
</div>
</div>
@@ -326,6 +326,11 @@ function handleImageError(e) {
font-size: 1.125rem;
}
/* Support contacts stack one per line */
.contact-line {
display: block;
}
/* Notes styling - rendered as escaped plain text, preserve author line breaks */
.notes-text {
white-space: pre-wrap;

View File

@@ -154,7 +154,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { applicationsApi } from '../../api'
import { applicationsApi, supportteamsApi } from '../../api'
import { apiError } from '../../utils/apiError'
const route = useRoute()
@@ -187,7 +187,7 @@ const supportTeams = ref([])
onMounted(async () => {
try {
// Load support teams
const teamsRes = await applicationsApi.getSupportTeams()
const teamsRes = await supportteamsApi.list()
supportTeams.value = teamsRes.data.data || []
// Load application if editing
@@ -198,7 +198,7 @@ onMounted(async () => {
form.value = {
appname: app.appname || '',
appdescription: app.appdescription || '',
supportteamid: app.supportteam?.supportteamid || '',
supportteamid: app.supportteamid || '',
isinstallable: app.isinstallable || false,
islicenced: app.islicenced || false,
isprinter: app.isprinter || false,

View File

@@ -34,7 +34,7 @@
<th>Application Name</th>
<th>Description</th>
<th>Support Team</th>
<th>App Owner</th>
<th>Contacts</th>
<th>Actions</th>
</tr>
</thead>
@@ -64,8 +64,8 @@
</span>
</td>
<td class="description">{{ app.appdescription || '-' }}</td>
<td>{{ app.supportteam?.teamname || '-' }}</td>
<td>{{ app.supportteam?.owner?.appowner || '-' }}</td>
<td>{{ app.supportteamname || '-' }}</td>
<td>{{ app.contacts && app.contacts.length ? app.contacts.map(c => c.name).join(', ') : '-' }}</td>
<td class="actions">
<router-link
:to="`/applications/${app.appid}`"