pre-tailwind removal bc the snackbar ain't coloring right

This commit is contained in:
2026-04-30 23:16:47 -06:00
parent 10ed917297
commit 1b2e1365d5
142 changed files with 1580 additions and 13508 deletions
+13 -15
View File
@@ -1,9 +1,10 @@
<template>
<v-app>
<v-app-bar density="comfortable" color="primary" v-if="!$route.meta.hideHeader">
<v-app-bar-title>My App, TheDisasterUnfoldingBeforeOurVeryEyesTheSequel</v-app-bar-title>
<v-app-bar-title>My App, The Disaster Unfolding Before Our Very Eyes: The Sequel</v-app-bar-title>
<v-btn variant="text" to="/">Home</v-btn>
<v-btn varient="text" to="/dashboard">Dashboard</v-btn>
<v-btn varient="text" to="/item">Item</v-btn>
<v-btn varient="text" to="/kitchen-sink">Kitchen Sink</v-btn>
<v-btn varient="text" to="/copilot-test">Copilot Test</v-btn>
<v-btn icon @click="toggleTheme">
@@ -29,31 +30,28 @@
<v-main>
<v-container fluid>
<router-view />
<AppSnackbar ref="snackbar" />
</v-container>
</v-main>
</v-app>
</template>
<script lang="ts" setup>
import HelloWorld from '@/components/HelloWorld.vue'
import { useDisplay, useTheme } from 'vuetify'
import { ref, provide } from 'vue'
const theme = useTheme()
function toggleTheme() {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
}
</script>
<!--
<HelloWorld />
<v-btn
class="m-2"
icon="mdi-theme-light-dark"
location="top right"
position="absolute"
@click="$vuetify.theme.cycle()"
/>
-->
// Snackbar
import AppSnackbar from './components/AppSnackbar.vue'
const snackbar = ref(null)
provide('snackbar', snackbar)
</script>
+36
View File
@@ -0,0 +1,36 @@
<template>
<v-snackbar
v-model="show"
:color="options.color ?? 'success'"
:timeout="options.timeout ?? 5000"
timer="bottom"
variant="flat"
theme="dark"
timer-color="green"
location="bottom center"
rounded="lg"
>
<v-icon start>{{ options.icon ?? 'mdi-check-circle-outline' }}</v-icon>
{{ options.message }} color: {{ options.color }}
</v-snackbar>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
const options = ref({})
function toast(message, opts = {}) {
options.value = { message, ...opts }
show.value = true
}
// Shorthand helpers
const success = (msg, opts = {}) => toast(msg, { color: 'success', icon: 'mdi-check-circle-outline', ...opts })
const error = (msg, opts = {}) => toast(msg, { color: 'error', icon: 'mdi-alert-circle-outline', ...opts })
const warning = (msg, opts = {}) => toast(msg, { color: 'warning', icon: 'mdi-alert-outline', ...opts })
const info = (msg, opts = {}) => toast(msg, { color: 'info', icon: 'mdi-information-outline', ...opts })
defineExpose({ toast, success, error, warning, info })
</script>
-14
View File
@@ -100,18 +100,4 @@
</script>
<style scoped>
@reference "../styles/tailwind.css";
/*
1. mixing helper classes and @apply for demonstration purposes only
2. the classes below are NOT wrapped in any CSS layer, so they "win" over everything else
*/
.hero-card {
@apply py-3 md:pr-[120px] w-full transition-none;
}
:deep(.v-card) {
@apply bg-gray-200;
@apply dark:bg-black dark:bg-linear-to-r dark:from-primary/50 dark:to-primary/30 dark:text-white/80;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
import EventBus, { ACTIONS } from '../index'
export const showSnackbar = (message, options = {}) => {
EventBus.emit(ACTIONS.SNACKBAR, { message, ...options })
}
+8
View File
@@ -0,0 +1,8 @@
import mitt from 'mitt'
export const ACTIONS = {
SNACKBAR: 'snackbar',
}
const EventBus = mitt()
export default EventBus
+4
View File
@@ -8,12 +8,16 @@ import { createVuetify } from 'vuetify'
import '@mdi/font/css/materialdesignicons.css'
import '../styles/layers.css'
import 'vuetify/styles'
import { VDateInput } from 'vuetify/labs/VDateInput'
export default createVuetify({
theme: {
defaultTheme: 'system',
utilities: false,
},
components: {
VDateInput,
},
display: {
mobileBreakpoint: 'md',
thresholds: {
+23 -11
View File
@@ -4,6 +4,8 @@ import KitchenSink from '../views/KitchenSink.vue'
import CopilotTest from '../views/CopilotTest.vue'
import Profile from '../views/Profile.vue'
import SystemNotice from '../views/SystemNotice.vue'
import Dashboard from '../views/Dashboard.vue'
import Item from '../views/Item.vue'
const routes = [
{
@@ -11,6 +13,27 @@ const routes = [
name: 'home',
component: HomeView
},
{
path: '/system-notice',
name: 'SystemNotice',
component: SystemNotice,
meta: { hideHeader: true }
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard
},
{
path: '/item',
name: 'Item',
component: Item
},
{
path: '/profile',
name: 'Profile',
component: Profile
},
{
path: '/kitchen-sink',
name: 'Kitchen Sink',
@@ -20,17 +43,6 @@ const routes = [
path: '/copilot-test',
name: 'Copilot Test',
component: CopilotTest
},
{
path: '/profile',
name: 'Profile',
component: Profile
},
{
path: '/system-notice',
name: 'SystemNotice',
component: SystemNotice,
meta: { hideHeader: true }
}
]
-10
View File
@@ -7,16 +7,6 @@
--font-body: "Roboto", sans-serif;
--font-mono: "Roboto Mono", monospace;
--color-background: rgb(var(--v-theme-background));
--color-surface: rgb(var(--v-theme-surface));
--color-on-surface: rgb(var(--v-theme-on-surface));
--color-primary: rgb(var(--v-theme-primary));
--color-success: rgb(var(--v-theme-success));
--color-info: rgb(var(--v-theme-info));
--color-warning: rgb(var(--v-theme-warning));
--color-error: rgb(var(--v-theme-error));
--breakpoint-*: initial;
--breakpoint-xs: 0px;
--breakpoint-sm: 600px;
+373
View File
@@ -0,0 +1,373 @@
<template>
<v-container fluid class="pa-6">
<!-- Page Header -->
<v-row align="center" class="mb-6">
<v-col>
<div class="text-h5 font-weight-medium text-high-emphasis">Dashboard</div>
<div class="text-body-2 text-medium-emphasis mt-1">Overview of all routed items</div>
</v-col>
<v-col cols="auto">
<v-btn-toggle v-model="viewMode" mandatory density="compact" variant="outlined" divided>
<v-btn value="cards" size="small">
<v-icon size="16" start>mdi-view-grid-outline</v-icon>
Cards
</v-btn>
<v-btn value="table" size="small">
<v-icon size="16" start>mdi-table</v-icon>
Table
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<!-- Stat Cards -->
<v-row class="mb-6">
<v-col v-for="stat in stats" :key="stat.label" cols="12" sm="6" md="3">
<v-card rounded="lg" variant="outlined" height="100%">
<v-card-text>
<div class="text-overline text-medium-emphasis mb-1">{{ stat.label }}</div>
<div class="text-h4 font-weight-medium text-high-emphasis">{{ stat.value }}</div>
<div class="mt-2">
<v-chip
:color="stat.chipColor"
size="x-small"
variant="tonal"
label
>
{{ stat.chipText }}
</v-chip>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Filter / Search Row -->
<v-row align="center" class="mb-4">
<v-col cols="12" sm="4" md="3">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Search items…"
density="compact"
variant="outlined"
hide-details
clearable
rounded="lg"
/>
</v-col>
<v-col cols="12" sm="3" md="2">
<v-select
v-model="filterStatus"
:items="statusOptions"
label="Status"
density="compact"
variant="outlined"
hide-details
rounded="lg"
clearable
/>
</v-col>
<v-col cols="12" sm="3" md="2">
<v-select
v-model="filterOrg"
:items="orgOptions"
label="Organization"
density="compact"
variant="outlined"
hide-details
rounded="lg"
clearable
/>
</v-col>
<v-spacer />
<v-col cols="auto">
<v-btn
color="primary"
prepend-icon="mdi-plus-circle-outline"
rounded="lg"
variant="flat"
@click="$router.push({ name: 'item', params: { mode: 'create' } })"
>
Submit item
</v-btn>
</v-col>
</v-row>
<!-- CARD VIEW -->
<template v-if="viewMode === 'cards'">
<v-row>
<v-col
v-for="item in filteredItems"
:key="item.id"
cols="12" sm="6" md="4" lg="3"
>
<v-card
rounded="lg"
variant="outlined"
height="100%"
class="item-card"
@click="$router.push({ name: 'item', params: { id: item.id, mode: 'view' } })"
>
<v-card-item>
<template #title>
<span class="text-body-1 font-weight-medium">{{ item.title }}</span>
</template>
<template #subtitle>
<span class="text-caption">{{ item.id }}</span>
</template>
<template #append>
<v-chip
:color="statusColor(item.status)"
size="x-small"
variant="tonal"
label
>
{{ item.status }}
</v-chip>
</template>
</v-card-item>
<v-card-text class="pt-0">
<!-- Progress -->
<div class="d-flex align-center justify-space-between mb-1">
<span class="text-caption text-medium-emphasis">
Step {{ item.step }} of {{ item.totalSteps }}
</span>
<span class="text-caption text-medium-emphasis">
{{ Math.round((item.step / item.totalSteps) * 100) }}%
</span>
</div>
<v-progress-linear
:model-value="(item.step / item.totalSteps) * 100"
:color="statusColor(item.status)"
rounded
height="4"
class="mb-3"
/>
<div class="text-caption text-medium-emphasis mb-3">{{ item.currentAction }}</div>
<!-- Org chips -->
<div class="d-flex flex-wrap gap-1">
<v-chip
v-for="org in item.organizations"
:key="org"
size="x-small"
variant="tonal"
color="blue-grey"
>
{{ org }}
</v-chip>
</div>
</v-card-text>
<v-divider />
<v-card-actions class="px-4 py-2">
<span class="text-caption text-disabled">{{ item.updated }}</span>
<v-spacer />
<v-chip
:color="priorityColor(item.priority)"
size="x-small"
variant="tonal"
label
>
{{ item.priority }}
</v-chip>
</v-card-actions>
</v-card>
</v-col>
<v-col v-if="filteredItems.length === 0" cols="12">
<v-empty-state
icon="mdi-file-search-outline"
title="No items found"
text="Try adjusting your search or filters."
/>
</v-col>
</v-row>
</template>
<!-- TABLE VIEW -->
<template v-else>
<v-card rounded="lg" variant="outlined">
<v-tabs v-model="activeTab" color="primary" density="compact">
<v-tab value="all">All ({{ items.length }})</v-tab>
<v-tab value="mine">Assigned to me ({{ assignedToMe.length }})</v-tab>
<v-tab value="drafts">Drafts ({{ drafts.length }})</v-tab>
<v-tab value="archived">Archived ({{ archived.length }})</v-tab>
</v-tabs>
<v-divider />
<v-data-table
:headers="tableHeaders"
:items="tabItems"
:search="search"
density="compact"
item-value="id"
hover
@click:row="onRowClick"
>
<!-- ID column -->
<template #item.id="{ item }">
<span class="text-caption text-medium-emphasis font-weight-medium">{{ item.id }}</span>
</template>
<!-- Title column -->
<template #item.title="{ item }">
<span class="text-body-2 font-weight-medium">{{ item.title }}</span>
</template>
<!-- Status column -->
<template #item.status="{ item }">
<v-chip
:color="statusColor(item.status)"
size="x-small"
variant="tonal"
label
>
{{ item.status }}
</v-chip>
</template>
<!-- Step column -->
<template #item.step="{ item }">
<span class="text-body-2">{{ item.step }}/{{ item.totalSteps }}</span>
</template>
<!-- Priority column -->
<template #item.priority="{ item }">
<v-chip
:color="priorityColor(item.priority)"
size="x-small"
variant="tonal"
label
>
{{ item.priority }}
</v-chip>
</template>
<!-- Updated column -->
<template #item.updated="{ item }">
<span class="text-caption text-medium-emphasis">{{ item.updated }}</span>
</template>
<!-- Row actions -->
<template #item.actions="{ item }">
<div class="d-flex ga-1 justify-end">
<v-btn
icon="mdi-eye-outline"
size="x-small"
variant="text"
@click.stop="$router.push({ name: 'item', params: { id: item.id, mode: 'view' } })"
/>
<v-btn
icon="mdi-pencil-outline"
size="x-small"
variant="text"
@click.stop="$router.push({ name: 'item', params: { id: item.id, mode: 'edit' } })"
/>
</div>
</template>
</v-data-table>
</v-card>
</template>
</v-container>
</template>
<script setup>
import { ref, computed } from 'vue'
// --- State ---
const viewMode = ref('cards')
const search = ref('')
const filterStatus = ref(null)
const filterOrg = ref(null)
const activeTab = ref('all')
// --- Static data ---
const stats = [
{ label: 'Total Items', value: 124, chipColor: 'blue-grey', chipText: '6 organizations' },
{ label: 'In Transit', value: 38, chipColor: 'primary', chipText: 'Active routing' },
{ label: 'Pending Review', value: 17, chipColor: 'warning', chipText: 'Awaiting action' },
{ label: 'Completed', value: 69, chipColor: 'success', chipText: 'This quarter' },
]
const statusOptions = ['In Transit', 'Pending', 'Completed']
const orgOptions = ['Acme Corp', 'Nexus Ltd', 'Gov Affairs', 'RegBody A', 'Agency B', 'Agency C', 'SupplyCo', 'BuildRight', 'DataOrg']
const items = ref([
{ id: '#DOC-2041', title: 'Q3 Partnership Agreement', type: 'Agreement', status: 'In Transit', step: 3, totalSteps: 5, currentAction: 'Step 3 of 5 — Legal review', organizations: ['Acme', 'Nexus', 'Gov'], priority: 'High', updated: '2h ago', mine: true, draft: false, arch: false },
{ id: '#DOC-2038', title: 'Vendor Onboarding Form', type: 'Onboarding', status: 'Completed', step: 5, totalSteps: 5, currentAction: 'All steps approved', organizations: ['Acme', 'SupplyCo'], priority: 'Normal', updated: 'Apr 22', mine: false, draft: false, arch: false },
{ id: '#DOC-2044', title: 'Regulatory Compliance Pkg', type: 'Compliance', status: 'Pending', step: 1, totalSteps: 4, currentAction: 'Step 1 of 4 — Intake', organizations: ['RegBody A', 'Acme'], priority: 'Urgent', updated: 'Today', mine: true, draft: false, arch: false },
{ id: '#DOC-2036', title: 'Inter-Agency MOU Draft', type: 'MOU', status: 'In Transit', step: 4, totalSteps: 5, currentAction: 'Step 4 of 5 — Final sign-off', organizations: ['Agency B', 'Agency C'], priority: 'Normal', updated: 'Yesterday',mine: false, draft: false, arch: false },
{ id: '#DOC-2029', title: 'Subcontractor NDA', type: 'NDA', status: 'Completed', step: 5, totalSteps: 5, currentAction: 'All steps approved', organizations: ['Acme', 'BuildRight'], priority: 'Normal', updated: 'Apr 18', mine: false, draft: false, arch: true },
{ id: '#DOC-2046', title: 'Data Sharing Agreement', type: 'Agreement', status: 'Pending', step: 0, totalSteps: 4, currentAction: 'Not yet started', organizations: ['DataOrg', 'Acme', 'Nexus'],priority: 'Normal', updated: 'Today', mine: false, draft: true, arch: false },
{ id: '#DOC-2047', title: 'Cross-Agency Data Protocol', type: 'Compliance', status: 'Pending', step: 0, totalSteps: 3, currentAction: 'Draft — not submitted', organizations: ['Agency B', 'DataOrg'], priority: 'High', updated: 'Today', mine: true, draft: true, arch: false },
])
// --- Computed ---
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchSearch = !search.value || item.title.toLowerCase().includes(search.value.toLowerCase()) || item.id.includes(search.value)
const matchStatus = !filterStatus.value || item.status === filterStatus.value
const matchOrg = !filterOrg.value || item.organizations.some(o => o.includes(filterOrg.value.split(' ')[0]))
return matchSearch && matchStatus && matchOrg
})
})
const assignedToMe = computed(() => items.value.filter(i => i.mine))
const drafts = computed(() => items.value.filter(i => i.draft))
const archived = computed(() => items.value.filter(i => i.arch))
const tabItems = computed(() => {
const map = { all: items.value, mine: assignedToMe.value, drafts: drafts.value, archived: archived.value }
return (map[activeTab.value] || items.value).filter(item => {
const matchSearch = !search.value || item.title.toLowerCase().includes(search.value.toLowerCase())
return matchSearch
})
})
const tableHeaders = [
{ title: 'ID', key: 'id', width: '110px' },
{ title: 'Title', key: 'title' },
{ title: 'Type', key: 'type', width: '110px' },
{ title: 'Status', key: 'status', width: '120px' },
{ title: 'Step', key: 'step', width: '70px' },
{ title: 'Priority', key: 'priority', width: '90px' },
{ title: 'Updated', key: 'updated', width: '100px' },
{ title: '', key: 'actions', width: '80px', sortable: false },
]
// --- Helpers ---
function statusColor(status) {
return { 'In Transit': 'primary', 'Pending': 'warning', 'Completed': 'success' }[status] ?? 'default'
}
function priorityColor(priority) {
return { 'Urgent': 'error', 'High': 'warning', 'Normal': 'default' }[priority] ?? 'default'
}
// --- Events ---
function onRowClick(_, { item }) {
// In real app: router.push({ name: 'item', params: { id: item.id, mode: 'view' } })
console.log('View item:', item.id)
}
</script>
<style scoped>
.item-card {
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.item-card:hover {
border-color: rgb(var(--v-theme-primary));
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.gap-1 {
gap: 4px;
}
</style>
+612
View File
@@ -0,0 +1,612 @@
<template>
<v-container fluid class="pa-6">
<!-- Page Header -->
<v-row align="center" class="mb-6">
<v-col cols="auto">
<v-btn
icon="mdi-arrow-left"
variant="text"
rounded="lg"
@click="$router.back()"
/>
</v-col>
<v-col>
<div class="text-h5 font-weight-medium text-high-emphasis">{{ pageTitle }}</div>
<div class="text-body-2 text-medium-emphasis mt-1">{{ pageSubtitle }}</div>
</v-col>
<v-col cols="auto" class="d-flex ga-2">
<!-- View mode actions -->
<template v-if="mode === 'view'">
<v-chip
:color="statusColor(item.status)"
variant="tonal"
label
class="mr-2"
>
{{ item.status }}
</v-chip>
<v-btn
variant="outlined"
prepend-icon="mdi-pencil-outline"
rounded="lg"
@click="mode = 'edit'"
>
Edit
</v-btn>
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-send-outline"
rounded="lg"
:disabled="item.status === 'Completed'"
@click="advanceRoute"
>
Advance route
</v-btn>
</template>
<!-- Create mode actions -->
<template v-else-if="mode === 'create'">
<v-btn variant="outlined" rounded="lg" @click="saveDraft">Save draft</v-btn>
<v-btn color="primary" variant="flat" rounded="lg" prepend-icon="mdi-send-outline" @click="submitItem">Submit for routing</v-btn>
</template>
<!-- Edit mode actions -->
<template v-else-if="mode === 'edit'">
<v-btn variant="outlined" rounded="lg" @click="cancelEdit">Cancel</v-btn>
<v-btn color="primary" variant="flat" rounded="lg" prepend-icon="mdi-content-save-outline" @click="saveEdit">Save changes</v-btn>
</template>
</v-col>
</v-row>
<!-- ===================== CREATE / EDIT MODE ===================== -->
<template v-if="mode === 'create' || mode === 'edit'">
<v-form ref="form" v-model="formValid">
<v-row>
<!-- Left column: Item details -->
<v-col cols="12" md="7">
<v-card rounded="lg" variant="outlined" class="mb-4">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Item details</v-card-title>
<v-divider />
<v-card-text>
<v-row density="comfortable">
<v-col cols="12">
<v-text-field
v-model="form.title"
label="Title"
placeholder="Describe the item briefly"
variant="outlined"
density="comfortable"
rounded="lg"
:rules="[v => !!v || 'Title is required']"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="form.type"
:items="itemTypes"
item-title="title"
item-value="value"
label="Item type"
variant="outlined"
density="comfortable"
rounded="lg"
:rules="[v => !!v || 'Type is required']"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="form.priority"
:items="priorityOptions"
item-title="title"
item-value="value"
label="Priority"
variant="outlined"
density="comfortable"
rounded="lg"
/>
</v-col>
<v-col cols="12" sm="6">
<v-date-input
clearable
label="Suspense Date"
v-model="form.dueDate"
:min="today"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="form.initiatingOrg"
:items="orgOptions"
item-title="title"
item-value="value"
label="Initiating organization"
variant="outlined"
density="comfortable"
rounded="lg"
:rules="[v => !!v || 'Organization is required']"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="form.notes"
label="Notes for reviewers"
placeholder="Add context, instructions, or background…"
variant="outlined"
density="comfortable"
rounded="lg"
rows="3"
auto-grow
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Attachments -->
<v-card rounded="lg" variant="outlined" class="mb-4">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Attachments</v-card-title>
<v-divider />
<v-card-text>
<v-file-input
v-model="form.files"
label="Attach files"
variant="outlined"
density="comfortable"
rounded="lg"
prepend-icon=""
prepend-inner-icon="mdi-paperclip"
multiple
chips
show-size
hint="Accepted: PDF, DOCX, XLSX, PNG, JPG"
persistent-hint
/>
</v-card-text>
</v-card>
</v-col>
<!-- Right column: Routing chain -->
<v-col cols="12" md="5">
<v-card rounded="lg" variant="outlined">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Routing chain</v-card-title>
<v-divider />
<v-card-text>
<v-timeline density="compact" side="end" truncate-line="both">
<v-timeline-item
v-for="(step, i) in form.routingChain"
:key="i"
:dot-color="routeStepColor(step.stepStatus)"
size="small"
>
<template #opposite>
<span class="text-caption font-weight-medium">{{ i + 1 }}</span>
</template>
<v-card variant="tonal" rounded="lg" :color="routeStepColor(step.stepStatus)">
<v-card-text class="pa-3">
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-body-2 font-weight-medium">{{ step.org }}</div>
<div class="text-caption text-medium-emphasis">{{ step.action }}</div>
</div>
<div class="d-flex ga-1 align-center">
<v-chip size="x-small" :color="routeStepColor(step.stepStatus)" variant="tonal" label>
{{ step.stepStatus }}
</v-chip>
<v-btn
v-if="mode === 'edit' || mode === 'create'"
icon="mdi-close"
size="x-small"
variant="text"
@click="removeRouteStep(i)"
/>
</div>
</div>
</v-card-text>
</v-card>
</v-timeline-item>
</v-timeline>
<!-- Add routing step -->
<v-expand-transition>
<div v-if="addingStep" class="mt-3">
<v-card variant="tonal" rounded="lg" color="primary">
<v-card-text class="pa-3">
<v-select
v-model="newStep.org"
:items="orgOptions"
item-title="title"
item-value="value"
label="Organization"
variant="outlined"
density="compact"
rounded="lg"
class="mb-2"
hide-details
/>
<v-select
v-model="newStep.action"
:items="actionOptions"
item-title="title"
item-value="value"
label="Action type"
variant="outlined"
density="compact"
rounded="lg"
class="mb-3"
hide-details
/>
<div class="d-flex ga-2 justify-end">
<v-btn size="small" variant="text" @click="addingStep = false">Cancel</v-btn>
<v-btn size="small" color="primary" variant="flat" @click="confirmAddStep">Add step</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</v-expand-transition>
<v-btn
v-if="!addingStep"
variant="tonal"
color="primary"
prepend-icon="mdi-plus"
block
rounded="lg"
class="mt-3"
@click="addingStep = true"
>
Add routing step
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</template>
<!-- ===================== VIEW MODE ===================== -->
<template v-else-if="mode === 'view'">
<v-row>
<!-- Left: Item info -->
<v-col cols="12" md="7">
<!-- Detail card -->
<v-card rounded="lg" variant="outlined" class="mb-4">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2 d-flex align-center">
Item details
<v-spacer />
<v-chip size="x-small" color="blue-grey" variant="tonal" label>{{ item.id }}</v-chip>
</v-card-title>
<v-divider />
<v-card-text>
<v-row density="comfortable">
<v-col cols="12" sm="6">
<div class="text-caption text-medium-emphasis mb-1">Title</div>
<div class="text-body-2 font-weight-medium">{{ item.title }}</div>
</v-col>
<v-col cols="12" sm="6">
<div class="text-caption text-medium-emphasis mb-1">Type</div>
<div class="text-body-2">{{ item.type }}</div>
</v-col>
<v-col cols="12" sm="6" class="mt-3">
<div class="text-caption text-medium-emphasis mb-1">Priority</div>
<v-chip :color="priorityColor(item.priority)" size="small" variant="tonal" label>
{{ item.priority }}
</v-chip>
</v-col>
<v-col cols="12" sm="6" class="mt-3">
<div class="text-caption text-medium-emphasis mb-1">Due date</div>
<div class="text-body-2">{{ item.dueDate || '—' }}</div>
</v-col>
<v-col cols="12" sm="6" class="mt-3">
<div class="text-caption text-medium-emphasis mb-1">Initiating org</div>
<div class="text-body-2">{{ item.initiatingOrg }}</div>
</v-col>
<v-col cols="12" sm="6" class="mt-3">
<div class="text-caption text-medium-emphasis mb-1">Last updated</div>
<div class="text-body-2">{{ item.updated }}</div>
</v-col>
<v-col cols="12" class="mt-3">
<div class="text-caption text-medium-emphasis mb-1">Notes</div>
<div class="text-body-2 text-medium-emphasis">{{ item.notes || 'No notes provided.' }}</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Attachments -->
<v-card rounded="lg" variant="outlined" class="mb-4">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Attachments</v-card-title>
<v-divider />
<v-list density="compact">
<v-list-item
v-for="file in item.attachments"
:key="file.name"
:prepend-icon="fileIcon(file.type)"
:title="file.name"
:subtitle="file.size"
>
<template #append>
<v-btn icon="mdi-download-outline" size="x-small" variant="text" />
</template>
</v-list-item>
<v-list-item v-if="!item.attachments?.length" title="No attachments" class="text-medium-emphasis" />
</v-list>
</v-card>
<!-- Activity log -->
<v-card rounded="lg" variant="outlined">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Activity log</v-card-title>
<v-divider />
<v-timeline density="compact" side="end" class="pa-4" truncate-line="both">
<v-timeline-item
v-for="event in item.activity"
:key="event.id"
:dot-color="event.color"
size="x-small"
>
<div class="text-body-2 font-weight-medium">{{ event.title }}</div>
<div class="text-caption text-medium-emphasis">{{ event.detail }}</div>
<div class="text-caption text-disabled mt-1">{{ event.time }}</div>
</v-timeline-item>
</v-timeline>
</v-card>
</v-col>
<!-- Right: Routing chain + progress -->
<v-col cols="12" md="5">
<!-- Progress summary -->
<v-card rounded="lg" variant="outlined" class="mb-4">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Routing progress</v-card-title>
<v-divider />
<v-card-text>
<div class="d-flex align-center justify-space-between mb-2">
<span class="text-body-2 text-medium-emphasis">Step {{ item.step }} of {{ item.totalSteps }}</span>
<span class="text-body-2 font-weight-medium">{{ Math.round((item.step / item.totalSteps) * 100) }}%</span>
</div>
<v-progress-linear
:model-value="(item.step / item.totalSteps) * 100"
:color="statusColor(item.status)"
rounded
height="6"
class="mb-4"
/>
<div class="text-caption text-medium-emphasis">{{ item.currentAction }}</div>
</v-card-text>
</v-card>
<!-- Routing chain (view) -->
<v-card rounded="lg" variant="outlined">
<v-card-title class="text-body-1 font-weight-medium pa-4 pb-2">Routing chain</v-card-title>
<v-divider />
<v-card-text>
<v-timeline density="compact" side="end" truncate-line="both">
<v-timeline-item
v-for="(step, i) in item.routingChain"
:key="i"
:dot-color="routeStepColor(step.stepStatus)"
size="small"
>
<template #opposite>
<span class="text-caption font-weight-medium">{{ i + 1 }}</span>
</template>
<v-card :color="routeStepColor(step.stepStatus)" variant="tonal" rounded="lg">
<v-card-text class="pa-3">
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-body-2 font-weight-medium">{{ step.org }}</div>
<div class="text-caption text-medium-emphasis">{{ step.action }}</div>
</div>
<v-chip size="x-small" :color="routeStepColor(step.stepStatus)" variant="tonal" label>
{{ step.stepStatus }}
</v-chip>
</div>
</v-card-text>
</v-card>
</v-timeline-item>
</v-timeline>
<!-- Advance route CTA if in transit -->
<v-alert
v-if="item.status === 'In Transit'"
type="info"
variant="tonal"
rounded="lg"
class="mt-4"
density="compact"
>
<template #text>
Action required at <strong>{{ currentStepOrg }}</strong>. Use "Advance route" to move forward.
</template>
</v-alert>
<v-alert
v-else-if="item.status === 'Completed'"
type="success"
variant="tonal"
rounded="lg"
class="mt-4"
density="compact"
text="All routing steps have been completed."
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<!-- Snackbar feedback -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
location="bottom right"
rounded="lg"
timeout="3000"
>
<v-icon start>{{ snackbar.icon }}</v-icon>
{{ snackbar.text }}
</v-snackbar>
</v-container>
</template>
<script setup>
import { ref, computed, inject } from 'vue'
const snackbar = inject('snackbar')
// --- Props ---
// In real app these come from router: props: ['id', 'initialMode']
const props = defineProps({
id: { type: String, default: null },
initialMode: { type: String, default: 'create' },
})
// --- Mode management ---
const mode = ref(props.id ? (props.initialMode ?? 'view') : 'create')
// --- Form state (create / edit) ---
const form = ref({
title: '',
type: '',
priority: 'Normal',
dueDate: '',
initiatingOrg:'',
notes: '',
files: [],
routingChain: [
{ org: 'Acme Corp', action: 'Creates and submits', stepStatus: 'Complete' },
{ org: 'Nexus Ltd', action: 'Legal & compliance review', stepStatus: 'Active' },
{ org: 'Gov Affairs', action: 'Final approval', stepStatus: 'Queued' },
],
})
const formValid = ref(false)
const addingStep = ref(false)
const newStep = ref({ org: '', action: '' })
// --- Mock item data (view mode) ---
const item = ref({
id: '#DOC-2041',
title: 'Q3 Partnership Agreement',
type: 'Agreement',
status: 'In Transit',
priority: 'High',
dueDate: '2024-06-30',
initiatingOrg: 'Acme Corp',
notes: 'This agreement governs the cross-agency data partnership for Q3. Legal review required before final sign-off.',
updated: '2 hours ago',
step: 3,
totalSteps: 5,
currentAction: 'Step 3 of 5 — Legal review at Nexus Ltd',
attachments: [
{ name: 'Q3_Partnership_v2.pdf', type: 'pdf', size: '1.2 MB' },
{ name: 'Exhibit_A_Terms.docx', type: 'docx', size: '340 KB' },
],
routingChain: [
{ org: 'Acme Corp', action: 'Initiated & submitted', stepStatus: 'Complete' },
{ org: 'Nexus Ltd', action: 'Legal & compliance review', stepStatus: 'Active' },
{ org: 'Gov Affairs', action: 'Regulatory sign-off', stepStatus: 'Queued' },
{ org: 'Acme Corp', action: 'Counter-signature', stepStatus: 'Queued' },
{ org: 'Archive', action: 'Record & close', stepStatus: 'Queued' },
],
activity: [
{ id: 1, title: 'Submitted for routing', detail: 'Jamie Doe — Acme Corp', time: 'Apr 28, 09:14', color: 'success' },
{ id: 2, title: 'Received by Nexus Ltd', detail: 'Auto-routed to legal queue', time: 'Apr 28, 09:15', color: 'primary' },
{ id: 3, title: 'Under legal review', detail: 'Assigned to K. Walsh', time: 'Apr 28, 11:30', color: 'warning' },
{ id: 4, title: 'Comment added', detail: 'K. Walsh — "Clause 4 needs revision"', time: 'Apr 29, 14:02', color: 'blue-grey' },
],
})
// --- Options ---
// Using {title, value} objects fixes a Vuetify 3 bug where plain string arrays
// fail to display the selected value until the field is blurred.
const toOpts = (arr) => arr.map(s => ({ title: s, value: s }))
const itemTypes = toOpts(['Agreement', 'MOU', 'NDA', 'Compliance', 'Onboarding', 'Other'])
const priorityOptions = toOpts(['Normal', 'High', 'Urgent'])
const orgOptions = toOpts(['Acme Corp', 'Nexus Ltd', 'Gov Affairs', 'RegBody A', 'Agency B', 'Agency C', 'SupplyCo', 'BuildRight', 'DataOrg'])
const actionOptions = toOpts(['Review & approve', 'Legal & compliance review', 'Sign-off', 'Counter-signature', 'Record & close', 'Other'])
// --- Computed ---
const pageTitle = computed(() => ({
create: 'Submit item',
view: item.value.title,
edit: `Editing — ${item.value.title}`,
})[mode.value])
const pageSubtitle = computed(() => ({
create: 'Route a new item through organizations',
view: `${item.value.id} · ${item.value.type}`,
edit: 'Make changes and save to update the record',
})[mode.value])
const currentStepOrg = computed(() => {
const step = item.value.routingChain?.find(s => s.stepStatus === 'Active')
return step?.org ?? '—'
})
const today = new Date().toISOString().split('T')[0]
// --- Methods ---
function statusColor(status) {
return { 'In Transit': 'primary', 'Pending': 'warning', 'Completed': 'success' }[status] ?? 'default'
}
function priorityColor(priority) {
return { 'Urgent': 'error', 'High': 'warning', 'Normal': 'default' }[priority] ?? 'default'
}
function routeStepColor(stepStatus) {
return { 'Complete': 'success', 'Active': 'primary', 'Queued': 'blue-grey' }[stepStatus] ?? 'default'
}
function fileIcon(type) {
return { pdf: 'mdi-file-pdf-box', docx: 'mdi-file-word-box', xlsx: 'mdi-file-excel-box' }[type] ?? 'mdi-file-outline'
}
function removeRouteStep(index) {
form.value.routingChain.splice(index, 1)
}
function confirmAddStep() {
if (newStep.value.org && newStep.value.action) {
form.value.routingChain.push({ org: newStep.value.org, action: newStep.value.action, stepStatus: 'Queued' })
newStep.value = { org: '', action: '' }
addingStep.value = false
}
}
function saveDraft() {
snackbar.value.toast('Draft saved', {
color: 'success',
icon: 'mdi-content-save-outline'
})
}
function submitItem() {
snackbar.value.success('Item submitted for routing')
}
function handleError() {
snackbar.value.error('Something went wrong', { timeout: 5000 })
}
function cancelEdit() {
mode.value = 'view'
}
function saveEdit() {
snackbar.value.toast('Changes saved', {
color: 'primary',
icon: 'mdi-check-circle-outline'
})
mode.value = 'view'
}
function advanceRoute() {
toast('Route advanced to next step', 'primary', 'mdi-arrow-right-circle-outline')
}
</script>
+2 -3
View File
@@ -59,12 +59,10 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, shallowRef } from 'vue'
const dialog = ref(false)
import { shallowRef } from 'vue'
const tab = shallowRef('tab-1')
const tabs = [
{
@@ -88,4 +86,5 @@
value: 'tab-4',
},
]
</script>