work-hub-server/apps/web/app/pages/projects/index.vue

147 lines
4.8 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
import type { TableColumn } from "@nuxt/ui";
import type { Project } from "@workspace/schema";
definePageMeta({
title: "Projects",
});
const { t } = useI18n();
const pageTitle = computed(() => t("projects.title"));
const pageDescription = computed(() => t("projects.description"));
const newProjectLabel = computed(() => t("projects.new"));
const emptyText = computed(() => t("projects.empty"));
const deleteTitle = computed(() => t("projects.delete.title"));
const deleteConfirm = computed(() => t("projects.delete.confirm"));
const deleteCancel = computed(() => t("projects.delete.cancel"));
const deleteDelete = computed(() => t("projects.delete.delete"));
const colName = computed(() => t("projects.columns.name"));
const colStatus = computed(() => t("projects.columns.status"));
const colDescription = computed(() => t("projects.columns.description"));
const colCreated = computed(() => t("projects.columns.created"));
const colActions = computed(() => t("projects.columns.actions"));
const { projects, loading, error, fetchProjects, deleteProject } = useProjectApi();
const deleting = ref<string | null>(null);
const confirmDelete = ref(false);
onMounted(() => fetchProjects());
function statusColor(status: string) {
if (status === "active") return "success" as const;
if (status === "archived") return "neutral" as const;
if (status === "completed") return "info" as const;
return "neutral" as const;
}
async function confirmAndDelete(id: string) {
deleting.value = id;
await deleteProject(id);
deleting.value = null;
confirmDelete.value = false;
}
const columns: TableColumn<Project>[] = [
{ accessorKey: "name", header: colName.value },
{ accessorKey: "status", header: colStatus.value },
{
accessorKey: "description",
header: colDescription.value,
cell: ({ row }) => row.getValue("description") ?? "—",
},
{ accessorKey: "createdAt", header: colCreated.value },
{ id: "actions", header: colActions.value },
];
</script>
<template>
<UPage>
<UPageHeader
:title="pageTitle"
:description="pageDescription"
:links="[{ label: newProjectLabel, icon: 'i-lucide-plus', to: '/projects/new', color: 'primary' }]" />
<UPageBody>
<UAlert v-if="error" color="error" variant="soft" icon="i-lucide-alert-circle" :title="error" class="mb-4" />
<UCard>
<UTable :columns="columns" :data="projects" :loading="loading" class="flex-1">
<template #status-cell="{ row }">
<UBadge
:label="row.getValue('status')"
:color="statusColor(row.getValue('status') as string)"
variant="subtle"
class="capitalize" />
</template>
<template #createdAt-cell="{ row }">
<span class="text-muted">{{ new Date(row.getValue("createdAt") as string).toLocaleDateString() }}</span>
</template>
<template #actions-cell="{ row }">
<div class="flex justify-end gap-1">
<UButton
icon="i-lucide-pencil"
color="neutral"
variant="ghost"
size="sm"
:to="`/projects/${row.original.id}/edit`"
aria-label="Edit" />
<UButton
icon="i-lucide-trash-2"
color="error"
variant="ghost"
size="sm"
:loading="deleting === row.original.id"
aria-label="Delete"
@click="
confirmDelete = true;
deleting = row.original.id;
" />
</div>
</template>
<template #empty>
<div class="py-12 text-center text-muted">
<UIcon name="i-lucide-folder-open" class="size-8 mx-auto mb-2" />
<p>{{ emptyText }}</p>
</div>
</template>
</UTable>
</UCard>
<UModal v-model:open="confirmDelete">
<template #header>
<h3 class="text-lg font-semibold">{{ deleteTitle }}</h3>
</template>
<template #body>
<p class="text-sm text-muted">{{ deleteConfirm }}</p>
</template>
<template #footer>
<div class="flex gap-3 justify-end">
<UButton
:label="deleteCancel"
icon="i-lucide-x"
color="neutral"
variant="outline"
@click="
confirmDelete = false;
deleting = null;
" />
<UButton
:label="deleteDelete"
icon="i-lucide-trash-2"
color="error"
@click="deleting && confirmAndDelete(deleting)" />
</div>
</template>
</UModal>
</UPageBody>
</UPage>
</template>