Initial release

This commit is contained in:
2025-05-30 17:07:06 +03:00
commit cd63464bc2
28 changed files with 988 additions and 0 deletions

140
src/views/Project.vue Normal file
View File

@@ -0,0 +1,140 @@
<template>
<div class="project-container">
<h3>{{ projectData.name }} (ID: {{ projectData.id }})</h3>
<div v-if="isLoadingInitialData">Загружаем карточки...</div>
<div v-else>
<div v-if="userstoriesForProject && userstoriesForProject.length === 0">Нет карточек на доске.</div>
<div v-else-if="userstoriesForProject" class="userstory-table-container">
<table>
<thead>
<tr>
<th v-for="header in tableHeaders" :key="header.key">{{ header.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="userstory in userstoriesForProject" :key="userstory.id">
<td v-for="header in tableHeaders" :key="`${userstory.id}-${header.key}`">
{{ getCellValue(userstory, header) }}
</td>
</tr>
<tr v-if="isLoadingAttributesForAnyStory">
<td :colspan="tableHeaders.length">Загружаем данные карточек...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from "vue";
import { useDataStore } from "@/stores/data";
import type { Project, Userstory, ProjectField, UserstoryStatusInfo } from "@/types/api";
interface TableHeader {
key: string;
label: string;
isAttribute: boolean;
attributeId?: number;
}
const props = defineProps<{
projectData: Project;
}>();
const dataStore = useDataStore();
const isLoadingInitialData = ref(true);
const isLoadingAttributesForAnyStory = ref(false);
const tableHeaders = computed<TableHeader[]>(() => {
const headers: TableHeader[] = [
{ key: "id", label: "ID", isAttribute: false },
{ key: "subject", label: "Заголовок карточки", isAttribute: false },
{ key: "status", label: "Статус", isAttribute: false },
];
const fields = dataStore.projectFieldsMap.get(props.projectData.id);
if (fields) {
// Сортируем поля, возможно не нужно
const sortedFields = [...fields].sort((a, b) => a.order - b.order);
sortedFields.forEach((field) => {
headers.push({
key: field.name,
label: field.name,
isAttribute: true,
attributeId: field.id,
});
});
}
return headers;
});
const userstoriesForProject = computed(() => {
return dataStore.userstoriesMap.get(props.projectData.id);
});
watch(
userstoriesForProject,
async (newUserstories) => {
if (newUserstories && newUserstories.length > 0) {
isLoadingAttributesForAnyStory.value = true;
const attributePromises = newUserstories
.filter((us) => !dataStore.userstoryAttributesMap.has(us.id))
.map((us) => dataStore.fetchUserstoryAttributes(us.id));
if (attributePromises.length > 0) {
await Promise.all(attributePromises);
}
isLoadingAttributesForAnyStory.value = false;
}
},
{ immediate: false },
);
onMounted(async () => {
isLoadingInitialData.value = true;
try {
await Promise.all([dataStore.fetchProjectFields(props.projectData.id), dataStore.fetchUserstories(props.projectData.id)]);
} catch (error) {
console.error(`Error loading data for project ${props.projectData.id}:`, error);
} finally {
isLoadingInitialData.value = false;
}
});
function getCellValue(userstory: Userstory, header: TableHeader): string | number | UserstoryStatusInfo | null {
if (!header.isAttribute) {
if (header.key === "status") {
return userstory.status_extra_info?.name || userstory.status.toString();
}
return userstory[header.key as keyof Userstory] ?? "";
} else {
if (header.attributeId === undefined) return "N/A (no attr ID)";
const attributes = dataStore.userstoryAttributesMap.get(userstory.id);
if (attributes) {
// Ключи для кастомных полей приходят как строки
return attributes[header.attributeId.toString()] ?? "";
}
return isLoadingAttributesForAnyStory.value ? "..." : "";
}
}
</script>
<style scoped>
.project-container {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.userstory-table-container {
max-height: 500px;
overflow-y: auto;
overflow-x: auto;
}
table thead tr th {
font-weight: bold;
}
</style>