Flask backend with Vue 3 frontend for shop floor machine management. Includes database schema export for MySQL shopdb_flask database. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
339 lines
9.6 KiB
Vue
339 lines
9.6 KiB
Vue
<template>
|
|
<div class="detail-page">
|
|
<div class="page-header">
|
|
<h2>Application Details</h2>
|
|
<div class="header-actions">
|
|
<router-link :to="`/applications/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
|
<router-link to="/applications" class="btn btn-secondary">Back to List</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else-if="app">
|
|
<!-- Hero Section -->
|
|
<div class="hero-card">
|
|
<div class="hero-image" v-if="app.image">
|
|
<img :src="`/images/applications/${app.image}`" :alt="app.appname" @error="handleImageError" />
|
|
</div>
|
|
<div class="hero-image placeholder" v-else>
|
|
<span class="placeholder-icon">📦</span>
|
|
</div>
|
|
<div class="hero-content">
|
|
<div class="hero-title">
|
|
<h1>{{ app.appname }}</h1>
|
|
</div>
|
|
<p class="hero-description" v-if="app.appdescription">{{ app.appdescription }}</p>
|
|
<div class="hero-meta">
|
|
<span v-if="app.isinstallable" class="badge badge-lg badge-info">Installable</span>
|
|
<span v-if="app.islicenced" class="badge badge-lg badge-warning">Licensed</span>
|
|
<span v-if="app.isprinter" class="badge badge-lg badge-secondary">Printer App</span>
|
|
<span v-if="app.ishidden" class="badge badge-lg badge-dark">Hidden</span>
|
|
</div>
|
|
<div class="hero-links" v-if="app.applicationlink || app.installpath || app.documentationpath">
|
|
<a v-if="app.applicationlink" :href="app.applicationlink" target="_blank" class="hero-link">
|
|
<span class="link-icon">🔗</span> Launch Application
|
|
</a>
|
|
<a v-if="app.installpath" :href="app.installpath" target="_blank" class="hero-link">
|
|
<span class="link-icon">⬇</span> Download Files
|
|
</a>
|
|
<a v-if="app.documentationpath" :href="app.documentationpath" target="_blank" class="hero-link">
|
|
<span class="link-icon">📄</span> Documentation
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="content-grid">
|
|
<!-- Left Column -->
|
|
<div class="content-column">
|
|
<!-- Support Info -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Support Information</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>
|
|
<span v-else>{{ app.supportteam?.teamname || '-' }}</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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Application Notes -->
|
|
<div class="section-card" v-if="app.applicationnotes">
|
|
<h3 class="section-title">Application Notes</h3>
|
|
<div class="notes-text" v-html="app.applicationnotes"></div>
|
|
</div>
|
|
|
|
<!-- Versions -->
|
|
<div class="section-card" v-if="versions.length > 0">
|
|
<h3 class="section-title">Available Versions</h3>
|
|
<div class="version-list">
|
|
<div v-for="ver in versions" :key="ver.appversionid" class="version-item">
|
|
<span class="version-number">v{{ ver.version }}</span>
|
|
<span class="version-date" v-if="ver.releasedate">{{ formatDate(ver.releasedate) }}</span>
|
|
<span class="version-notes" v-if="ver.notes">{{ ver.notes }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="content-column">
|
|
<!-- Installed On PCs -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Installed On ({{ installedOn.length }} PCs)</h3>
|
|
<div v-if="installedOn.length > 0" class="pc-list">
|
|
<router-link
|
|
v-for="install in installedOn"
|
|
:key="install.id"
|
|
:to="`/pcs/${install.machineid}`"
|
|
class="pc-item"
|
|
>
|
|
<div class="pc-info">
|
|
<span class="pc-name">{{ install.machine?.machinenumber || `PC #${install.machineid}` }}</span>
|
|
<span class="pc-alias" v-if="install.machine?.alias">{{ install.machine.alias }}</span>
|
|
</div>
|
|
<div class="pc-version" v-if="install.version">
|
|
v{{ install.version }}
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
<div v-else class="empty-state">
|
|
<p>Not installed on any PCs</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Footer -->
|
|
<div class="audit-footer">
|
|
<span>Created {{ formatDate(app.createddate) }}</span>
|
|
<span>Modified {{ formatDate(app.modifieddate) }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="card">
|
|
<p style="text-align: center; color: var(--text-light);">Application not found</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { applicationsApi } from '../../api'
|
|
|
|
const route = useRoute()
|
|
|
|
const loading = ref(true)
|
|
const app = ref(null)
|
|
const versions = ref([])
|
|
const installedOn = ref([])
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
// Load application details
|
|
const response = await applicationsApi.get(route.params.id)
|
|
app.value = response.data.data
|
|
|
|
// Load versions
|
|
try {
|
|
const versionsRes = await applicationsApi.getVersions(route.params.id)
|
|
versions.value = versionsRes.data.data || []
|
|
} catch (e) {
|
|
console.log('No versions data')
|
|
}
|
|
|
|
// Load installed on which PCs
|
|
try {
|
|
const installedRes = await applicationsApi.getInstalledOn(route.params.id)
|
|
installedOn.value = installedRes.data.data || []
|
|
} catch (e) {
|
|
console.log('No installed data')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading application:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-'
|
|
return new Date(dateStr).toLocaleDateString()
|
|
}
|
|
|
|
function handleImageError(e) {
|
|
e.target.style.display = 'none'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Application-specific styles - shared styles are in global style.css */
|
|
|
|
/* Hero description (app-specific) */
|
|
.hero-description {
|
|
color: var(--text-light);
|
|
margin: 0;
|
|
font-size: 1.125rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Hero links (app-specific) */
|
|
.hero-links {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: auto;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.hero-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.25rem;
|
|
background: var(--bg);
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
font-size: 1.125rem;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.hero-link:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
.link-icon {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
/* Placeholder image */
|
|
.hero-image.placeholder {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.placeholder-icon {
|
|
font-size: 5rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Version List */
|
|
.version-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.version-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.version-number {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
font-weight: 600;
|
|
font-size: 1.125rem;
|
|
color: var(--link);
|
|
}
|
|
|
|
.version-date {
|
|
font-size: 1rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.version-notes {
|
|
font-size: 1rem;
|
|
color: var(--text-light);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* PC List */
|
|
.pc-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.pc-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem;
|
|
background: var(--bg);
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.pc-item:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
.pc-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pc-name {
|
|
font-weight: 500;
|
|
font-size: 1.125rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.pc-alias {
|
|
font-size: 1rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.pc-version {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
font-size: 1rem;
|
|
color: var(--primary);
|
|
background: var(--bg-card);
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 2.5rem;
|
|
color: var(--text-light);
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
/* Notes styling */
|
|
.notes-text :deep(a) {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.notes-text :deep(a:hover) {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|