initial
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div v-if="overview">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<h1 class="m-0 text-2xl font-bold">{{ overview.project.name }}</h1>
|
||||
<Tag v-if="overview.project.status === 'Archived'" value="Archived" severity="warning" />
|
||||
</div>
|
||||
<p v-if="overview.project.description" class="-mt-3 mb-4 text-slate-500 dark:text-slate-400">{{ overview.project.description }}</p>
|
||||
|
||||
<div class="mb-3 grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-3">
|
||||
<Card class="[&_.p-card-body]:pt-3">
|
||||
<template #content>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Members</div>
|
||||
<div class="text-[1.8rem] font-bold">{{ overview.memberCount }}</div>
|
||||
</template>
|
||||
</Card>
|
||||
<Card class="[&_.p-card-body]:pt-3">
|
||||
<template #content>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Documents</div>
|
||||
<div class="text-[1.8rem] font-bold">{{ overview.documentCount }}</div>
|
||||
</template>
|
||||
</Card>
|
||||
<Card v-for="(count, status) in taskCounts" :key="status" class="[&_.p-card-body]:pt-3">
|
||||
<template #content>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">{{ statusLabel(status) }}</div>
|
||||
<div class="text-[1.8rem] font-bold">{{ count }}</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex flex-col gap-3 lg:flex-row">
|
||||
<Card class="min-w-0 flex-1">
|
||||
<template #title>Recent Tasks</template>
|
||||
<template #content>
|
||||
<div class="overflow-x-auto">
|
||||
<DataTable :value="overview.recentTasks" emptyMessage="No tasks">
|
||||
<Column field="title" header="Title" />
|
||||
<Column field="status" header="Status" style="width: 120px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status" :severity="statusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<Card class="min-w-0 flex-1">
|
||||
<template #title>Recent Documents</template>
|
||||
<template #content>
|
||||
<div class="overflow-x-auto">
|
||||
<DataTable :value="overview.recentDocuments" emptyMessage="No documents">
|
||||
<Column field="title" header="Title" />
|
||||
<Column header="Updated" style="width: 150px">
|
||||
<template #body="{ data }">
|
||||
<span class="text-slate-500 dark:text-slate-400">{{ formatDate(data.updatedAt) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button v-if="auth.can('projects', 'edit')" icon="pi pi-pencil" label="Edit project" outlined @click="openEdit" />
|
||||
<Button
|
||||
v-if="auth.can('projects', 'delete') && overview.project.status !== 'Archived'"
|
||||
icon="pi pi-archive"
|
||||
label="Archive project"
|
||||
severity="warning"
|
||||
outlined
|
||||
@click="confirmArchive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="editDialog" header="Edit Project" :modal="true" style="width: min(480px, 92vw)">
|
||||
<div class="field">
|
||||
<label for="ename">Name</label>
|
||||
<InputText id="ename" v-model.trim="editName" class="w-full" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="edesc">Description</label>
|
||||
<Textarea id="edesc" v-model="editDescription" rows="3" class="w-full" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" text @click="editDialog = false" />
|
||||
<Button label="Save" :loading="saving" @click="onSave" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center p-[60px]">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getProjectOverview, updateProject, deleteProject } from '../../services/backend'
|
||||
import { errorMessage } from '../../services/api'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import type { ProjectOverview } from '../../types'
|
||||
|
||||
const route = useRoute()
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const overview = ref<ProjectOverview | null>(null)
|
||||
const editDialog = ref(false)
|
||||
const editName = ref('')
|
||||
const editDescription = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
const taskCounts = computed(() => {
|
||||
const counts = overview.value?.taskCountsByStatus ?? {}
|
||||
const order = ['Todo', 'InProgress', 'Done', 'Cancelled']
|
||||
const result: Record<string, number> = {}
|
||||
for (const key of order) {
|
||||
if (counts[key] !== undefined) {
|
||||
result[key] = counts[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
overview.value = await getProjectOverview(String(route.params.id))
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
if (overview.value) {
|
||||
editName.value = overview.value.project.name
|
||||
editDescription.value = overview.value.project.description ?? ''
|
||||
editDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!overview.value || !editName.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Name is required', life: 3000 })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await updateProject(overview.value.project.id, {
|
||||
name: editName.value,
|
||||
description: editDescription.value || null,
|
||||
status: overview.value.project.status,
|
||||
})
|
||||
editDialog.value = false
|
||||
toast.add({ severity: 'success', summary: 'Saved', life: 3000 })
|
||||
await load()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmArchive() {
|
||||
if (!overview.value) return
|
||||
confirm.require({
|
||||
message: `Archive project "${overview.value.project.name}"?`,
|
||||
header: 'Archive Project',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Archive',
|
||||
rejectLabel: 'Cancel',
|
||||
accept: async () => {
|
||||
try {
|
||||
await deleteProject(overview.value!.project.id)
|
||||
toast.add({ severity: 'success', summary: 'Project archived', life: 3000 })
|
||||
window.location.href = '/projects'
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return status.replace(/([A-Z])/g, ' $1').trim()
|
||||
}
|
||||
|
||||
function statusSeverity(status: string) {
|
||||
return status === 'Done' ? 'success' : status === 'InProgress' ? 'info' : status === 'Cancelled' ? 'danger' : 'secondary'
|
||||
}
|
||||
|
||||
function formatDate(value: string) {
|
||||
return new Date(value).toLocaleDateString()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
Reference in New Issue
Block a user