1399 lines
47 KiB
Vue
1399 lines
47 KiB
Vue
<template>
|
|
<div class="project-management">
|
|
<CrudDataTable
|
|
ref="tableRef"
|
|
title="Projekte"
|
|
entity-name="Projekt"
|
|
entity-name-article="ein"
|
|
:columns="projectColumns"
|
|
data-source="/api/projects"
|
|
storage-key="projectTableColumns"
|
|
:show-view-button="canView"
|
|
:show-edit-button="canEdit"
|
|
:show-delete-button="canDelete"
|
|
:show-export-button="canExport"
|
|
@view="viewProject"
|
|
@create="openNewProjectDialog"
|
|
@edit="editProject"
|
|
@delete="confirmDelete"
|
|
@data-loaded="onDataLoaded"
|
|
>
|
|
<!-- Custom Filter Buttons -->
|
|
<template #filter-buttons="{ loadData }">
|
|
<div class="flex gap-2 mb-4">
|
|
<Button
|
|
label="Alle"
|
|
:outlined="typeFilter !== 'all'"
|
|
@click="filterByType('all', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Beruflich"
|
|
:outlined="typeFilter !== 'business'"
|
|
@click="filterByType('business', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Privat"
|
|
:outlined="typeFilter !== 'private'"
|
|
@click="filterByType('private', loadData)"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Custom Column Templates -->
|
|
<template #body-name="{ data }">
|
|
<div class="font-semibold">{{ data.name }}</div>
|
|
</template>
|
|
|
|
<template #body-projectNumber="{ data }">
|
|
{{ data.projectNumber }}
|
|
</template>
|
|
|
|
<template #body-customer="{ data }">
|
|
<div v-if="data.customer">{{ data.customer.companyName }}</div>
|
|
<span v-else class="text-500">Kein Kunde zugewiesen</span>
|
|
</template>
|
|
|
|
<template #body-status="{ data }">
|
|
<Tag v-if="data.status" :value="data.status.name" :style="{ backgroundColor: data.status.color }" />
|
|
<span v-else class="text-500">Kein Status</span>
|
|
</template>
|
|
|
|
<template #body-orderNumber="{ data }">
|
|
{{ data.orderNumber }}
|
|
</template>
|
|
|
|
<template #body-orderDate="{ data }">
|
|
{{ formatDate(data.orderDate) }}
|
|
</template>
|
|
|
|
<template #body-startDate="{ data }">
|
|
{{ formatDate(data.startDate) }}
|
|
</template>
|
|
|
|
<template #body-endDate="{ data }">
|
|
{{ formatDate(data.endDate) }}
|
|
</template>
|
|
|
|
<template #body-budget="{ data }">
|
|
<div v-if="data.budget" class="text-right">{{ formatCurrency(data.budget) }}</div>
|
|
</template>
|
|
|
|
<template #body-hourContingent="{ data }">
|
|
<div v-if="data.hourContingent" class="text-right">{{ data.hourContingent }} h</div>
|
|
</template>
|
|
|
|
<template #body-type="{ data }">
|
|
<Tag :value="data.isPrivate ? 'Privat' : 'Beruflich'" :severity="data.isPrivate ? 'info' : 'success'" />
|
|
</template>
|
|
|
|
<template #body-createdAt="{ data }">
|
|
{{ formatDate(data.createdAt) }}
|
|
</template>
|
|
|
|
<template #body-updatedAt="{ data }">
|
|
{{ formatDate(data.updatedAt) }}
|
|
</template>
|
|
</CrudDataTable>
|
|
|
|
<!-- Project Dialog -->
|
|
<Dialog
|
|
v-model:visible="projectDialog"
|
|
:header="editingProject?.id ? 'Projekt bearbeiten' : 'Neues Projekt'"
|
|
:modal="true"
|
|
:style="{ width: '900px' }"
|
|
:closable="!saving"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Basic Information -->
|
|
<div class="font-semibold text-lg">Grunddaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="name">Projektname *</label>
|
|
<InputText
|
|
id="name"
|
|
v-model="editingProject.name"
|
|
:class="{ 'p-invalid': submitted && !editingProject.name }"
|
|
:disabled="saving"
|
|
/>
|
|
<small v-if="submitted && !editingProject.name" class="p-error">Projektname ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="description">Beschreibung</label>
|
|
<Textarea
|
|
id="description"
|
|
v-model="editingProject.description"
|
|
rows="3"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="customer">Kunde</label>
|
|
<Select
|
|
id="customer"
|
|
v-model="editingProject.customer"
|
|
:options="customers"
|
|
option-label="companyName"
|
|
placeholder="Kunde auswählen"
|
|
filter
|
|
:disabled="saving"
|
|
show-clear
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="status">Status</label>
|
|
<Select
|
|
id="status"
|
|
v-model="editingProject.status"
|
|
:options="statuses"
|
|
option-label="name"
|
|
placeholder="Status auswählen"
|
|
:disabled="saving"
|
|
show-clear
|
|
>
|
|
<template #option="slotProps">
|
|
<div class="flex align-items-center gap-2">
|
|
<span
|
|
class="inline-block w-3 h-3 border-round"
|
|
:style="{ backgroundColor: slotProps.option.color }"
|
|
></span>
|
|
<span>{{ slotProps.option.name }}</span>
|
|
</div>
|
|
</template>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="isPrivate">Typ</label>
|
|
<div class="flex align-items-center gap-3 mt-2">
|
|
<div class="flex align-items-center">
|
|
<RadioButton
|
|
id="business"
|
|
v-model="editingProject.isPrivate"
|
|
:value="false"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="business" class="ml-2">Beruflich</label>
|
|
</div>
|
|
<div class="flex align-items-center">
|
|
<RadioButton
|
|
id="private"
|
|
v-model="editingProject.isPrivate"
|
|
:value="true"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="private" class="ml-2">Privat</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Numbers & Dates -->
|
|
<div class="font-semibold text-lg mt-4">Nummern & Daten</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="projectNumber">Projektnummer</label>
|
|
<InputText
|
|
id="projectNumber"
|
|
v-model="editingProject.projectNumber"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="orderNumber">Bestellnummer</label>
|
|
<InputText
|
|
id="orderNumber"
|
|
v-model="editingProject.orderNumber"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="orderDate">Bestelldatum</label>
|
|
<DatePicker
|
|
id="orderDate"
|
|
v-model="editingProject.orderDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="startDate">Startdatum</label>
|
|
<DatePicker
|
|
id="startDate"
|
|
v-model="editingProject.startDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="endDate">Enddatum</label>
|
|
<DatePicker
|
|
id="endDate"
|
|
v-model="editingProject.endDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget & Hours -->
|
|
<div class="font-semibold text-lg mt-4">Budget & Kontingent</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="budget">Budget (€)</label>
|
|
<InputNumber
|
|
id="budget"
|
|
v-model="editingProject.budget"
|
|
mode="currency"
|
|
currency="EUR"
|
|
locale="de-DE"
|
|
:min="0"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="hourContingent">Stundenkontingent</label>
|
|
<InputNumber
|
|
id="hourContingent"
|
|
v-model="editingProject.hourContingent"
|
|
suffix=" h"
|
|
:min-fraction-digits="0"
|
|
:max-fraction-digits="2"
|
|
:min="0"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents Section (only for existing projects) -->
|
|
<div v-if="editingProject.id" class="mt-4">
|
|
<div class="font-semibold text-lg mb-3">Dokumente</div>
|
|
<DocumentUpload
|
|
entity-type="project"
|
|
:entity-id="editingProject.id"
|
|
:can-upload="true"
|
|
:can-delete="true"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Git Repositories Section (only for existing projects) -->
|
|
<div v-if="editingProject.id" class="mt-4">
|
|
<div class="font-semibold text-lg mb-3">Git Repositories</div>
|
|
|
|
<!-- Repository List -->
|
|
<DataTable
|
|
v-if="editGitRepositories.length > 0"
|
|
:value="editGitRepositories"
|
|
class="mb-3"
|
|
striped-rows
|
|
>
|
|
<Column field="name" header="Name" style="width: 25%">
|
|
<template #body="{ data }">
|
|
<div class="font-medium">{{ data.name }}</div>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="url" header="URL" style="width: 35%">
|
|
<template #body="{ data }">
|
|
<a :href="data.url" target="_blank" class="text-primary hover:underline text-sm">
|
|
{{ data.url }}
|
|
</a>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="provider" header="Provider" style="width: 15%">
|
|
<template #body="{ data }">
|
|
<Tag :value="data.provider || 'local'" severity="info" />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="branch" header="Branch" style="width: 15%">
|
|
<template #body="{ data }">
|
|
<code class="text-sm">{{ data.branch || 'main' }}</code>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column style="width: 10%">
|
|
<template #body="{ data }">
|
|
<div class="flex gap-2 justify-end">
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
size="small"
|
|
text
|
|
@click="editGitRepo(data)"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
size="small"
|
|
text
|
|
severity="danger"
|
|
@click="confirmDeleteGitRepo(data)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<div v-else class="p-4 border-round border-1 surface-border text-center text-500 mb-3">
|
|
<i class="pi pi-github text-3xl mb-2"></i>
|
|
<p>Keine Git-Repositories verknüpft</p>
|
|
</div>
|
|
|
|
<!-- Add Repository Button -->
|
|
<Button
|
|
label="Repository hinzufügen"
|
|
icon="pi pi-plus"
|
|
size="small"
|
|
@click="openAddGitRepoDialog"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="projectDialog = false" text :disabled="saving" />
|
|
<Button label="Speichern" @click="saveProject" :loading="saving" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Git Repository Dialog -->
|
|
<Dialog
|
|
v-model:visible="gitRepoDialog"
|
|
:header="editingGitRepo?.id ? 'Repository bearbeiten' : 'Repository hinzufügen'"
|
|
:modal="true"
|
|
:style="{ width: '600px' }"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoName">Name *</label>
|
|
<InputText
|
|
id="gitRepoName"
|
|
v-model="editingGitRepo.name"
|
|
:class="{ 'p-invalid': gitRepoSubmitted && !editingGitRepo.name }"
|
|
placeholder="z.B. Main Repository"
|
|
/>
|
|
<small v-if="gitRepoSubmitted && !editingGitRepo.name" class="p-error">Name ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoUrl">URL *</label>
|
|
<InputText
|
|
id="gitRepoUrl"
|
|
v-model="editingGitRepo.url"
|
|
:class="{ 'p-invalid': gitRepoSubmitted && !editingGitRepo.url }"
|
|
placeholder="https://github.com/user/repo"
|
|
/>
|
|
<small v-if="gitRepoSubmitted && !editingGitRepo.url" class="p-error">URL ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoProvider">Provider</label>
|
|
<Select
|
|
id="gitRepoProvider"
|
|
v-model="editingGitRepo.provider"
|
|
:options="gitProviders"
|
|
option-label="label"
|
|
option-value="value"
|
|
placeholder="Provider auswählen"
|
|
/>
|
|
<small class="text-500">Leer lassen für lokale Repositories</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoBranch">Branch</label>
|
|
<InputText
|
|
id="gitRepoBranch"
|
|
v-model="editingGitRepo.branch"
|
|
placeholder="main"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Test Connection Section -->
|
|
<div v-if="editingGitRepo.url && editingGitRepo.provider" class="p-3 border-round border-1 surface-border">
|
|
<div class="flex justify-between align-items-center mb-2">
|
|
<span class="font-medium text-sm">Verbindung testen</span>
|
|
<Button
|
|
label="Testen"
|
|
icon="pi pi-check-circle"
|
|
size="small"
|
|
outlined
|
|
:loading="testingConnection"
|
|
@click="testRepositoryConnection"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="connectionTestResult" class="mt-2">
|
|
<div v-if="connectionTestResult.success" class="flex align-items-start gap-2 p-2 border-round surface-ground border-1 border-green-500">
|
|
<i class="pi pi-check-circle text-green-600 mt-1"></i>
|
|
<div class="flex-1">
|
|
<div class="font-semibold text-sm text-green-700 dark:text-green-400">Verbindung erfolgreich</div>
|
|
<div class="text-xs mt-1">{{ connectionTestResult.message }}</div>
|
|
<div v-if="connectionTestResult.details" class="text-xs mt-1 text-600">
|
|
<div v-if="connectionTestResult.details.defaultBranch">
|
|
Standard-Branch: <code>{{ connectionTestResult.details.defaultBranch }}</code>
|
|
</div>
|
|
<div v-if="connectionTestResult.details.commitCount">
|
|
Commits gefunden: {{ connectionTestResult.details.commitCount }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="flex align-items-start gap-2 p-2 border-round surface-ground border-1 border-red-500">
|
|
<i class="pi pi-times-circle text-red-600 mt-1"></i>
|
|
<div class="flex-1">
|
|
<div class="font-semibold text-sm text-red-700 dark:text-red-400">Verbindung fehlgeschlagen</div>
|
|
<div class="text-xs mt-1">{{ connectionTestResult.message }}</div>
|
|
<div v-if="connectionTestResult.error" class="text-xs mt-1 text-600 font-mono">
|
|
{{ connectionTestResult.error }}
|
|
</div>
|
|
<div v-if="connectionTestResult.details" class="text-xs mt-2 text-600">
|
|
<div v-if="connectionTestResult.details.hint" class="font-semibold">
|
|
💡 {{ connectionTestResult.details.hint }}
|
|
</div>
|
|
<div v-if="connectionTestResult.details.owner">
|
|
Owner: {{ connectionTestResult.details.owner }}
|
|
</div>
|
|
<div v-if="connectionTestResult.details.repo">
|
|
Repo: {{ connectionTestResult.details.repo }}
|
|
</div>
|
|
<div v-if="connectionTestResult.details.branch">
|
|
Branch: {{ connectionTestResult.details.branch }}
|
|
</div>
|
|
<div v-if="connectionTestResult.details.defaultBranch">
|
|
Standard-Branch: {{ connectionTestResult.details.defaultBranch }}
|
|
</div>
|
|
</div>
|
|
<div v-if="connectionTestResult.trace" class="text-xs mt-2 p-2 surface-100 dark:surface-800 border-round overflow-auto max-h-10rem font-mono">
|
|
{{ connectionTestResult.trace }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoLocalPath">Lokaler Pfad (optional)</label>
|
|
<InputText
|
|
id="gitRepoLocalPath"
|
|
v-model="editingGitRepo.localPath"
|
|
placeholder="/pfad/zum/repository"
|
|
/>
|
|
<small class="text-500">Nur für lokale Git-Repositories erforderlich</small>
|
|
</div>
|
|
|
|
<div v-if="editingGitRepo.provider && editingGitRepo.provider !== 'local'" class="flex flex-col gap-2">
|
|
<label for="gitRepoToken">Access Token (optional)</label>
|
|
<InputText
|
|
id="gitRepoToken"
|
|
v-model="editingGitRepo.accessToken"
|
|
type="password"
|
|
placeholder="Token für private Repositories"
|
|
/>
|
|
<small class="text-500">Für private Repositories oder höhere Rate Limits</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="gitRepoDescription">Beschreibung</label>
|
|
<Textarea
|
|
id="gitRepoDescription"
|
|
v-model="editingGitRepo.description"
|
|
rows="2"
|
|
placeholder="Optional: Beschreibung des Repositories"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="gitRepoDialog = false" text :disabled="savingGitRepo" />
|
|
<Button label="Speichern" @click="saveGitRepo" :loading="savingGitRepo" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Git Repo Confirmation Dialog -->
|
|
<Dialog
|
|
v-model:visible="deleteGitRepoDialog"
|
|
header="Repository entfernen"
|
|
:modal="true"
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<div class="flex align-items-center gap-3">
|
|
<i class="pi pi-exclamation-triangle text-4xl text-orange-500" />
|
|
<span>Möchten Sie das Repository <b>{{ gitRepoToDelete?.name }}</b> wirklich entfernen?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="deleteGitRepoDialog = false" text :disabled="deletingGitRepo" />
|
|
<Button label="Entfernen" @click="deleteGitRepo" severity="danger" :loading="deletingGitRepo" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog
|
|
v-model:visible="deleteDialog"
|
|
header="Projekt löschen"
|
|
:modal="true"
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<div class="flex align-items-center gap-3">
|
|
<i class="pi pi-exclamation-triangle text-4xl text-red-500" />
|
|
<span>Möchten Sie das Projekt <b>{{ projectToDelete?.name }}</b> wirklich löschen?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="deleteDialog = false" text :disabled="deleting" />
|
|
<Button label="Löschen" @click="deleteProject" severity="danger" :loading="deleting" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- View Project Dialog -->
|
|
<Dialog
|
|
v-model:visible="viewDialog"
|
|
header="Projekt anzeigen"
|
|
:modal="true"
|
|
:style="{ width: '1400px' }"
|
|
>
|
|
<Tabs value="0">
|
|
<TabList>
|
|
<Tab value="0">Projektdaten</Tab>
|
|
<Tab value="1">Git Repositories</Tab>
|
|
</TabList>
|
|
<TabPanels>
|
|
<!-- Project Data Tab -->
|
|
<TabPanel value="0">
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Basic Information -->
|
|
<div class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Grunddaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Projektname</label>
|
|
<div class="text-900">{{ viewingProject.name || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Projektnummer</label>
|
|
<div class="text-900">{{ viewingProject.projectNumber || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label class="font-medium text-sm text-500">Beschreibung</label>
|
|
<div class="text-900 whitespace-pre-wrap">{{ viewingProject.description || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Kunde</label>
|
|
<div class="text-900">{{ viewingProject.customer?.companyName || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Status</label>
|
|
<Tag v-if="viewingProject.status" :value="viewingProject.status.name" :style="{ backgroundColor: viewingProject.status.color }" />
|
|
<div v-else class="text-900">-</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Typ</label>
|
|
<Tag :value="viewingProject.isPrivate ? 'Privat' : 'Beruflich'" :severity="viewingProject.isPrivate ? 'info' : 'success'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Numbers & Dates -->
|
|
<div class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Nummern & Daten</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Bestellnummer</label>
|
|
<div class="text-900">{{ viewingProject.orderNumber || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Bestelldatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.orderDate) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Startdatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.startDate) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Enddatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.endDate) || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget & Hours -->
|
|
<div class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Budget & Kontingent</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Budget</label>
|
|
<div class="text-900">{{ viewingProject.budget ? formatCurrency(viewingProject.budget) : '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Stundenkontingent</label>
|
|
<div class="text-900">{{ viewingProject.hourContingent ? `${viewingProject.hourContingent} h` : '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents Section -->
|
|
<div class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Dokumente</div>
|
|
<DocumentUpload
|
|
v-if="viewingProject.id"
|
|
entity-type="project"
|
|
:entity-id="viewingProject.id"
|
|
:can-upload="false"
|
|
:can-delete="false"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Timestamps -->
|
|
<div class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Metadaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Erstellt am</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.createdAt) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Zuletzt geändert</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.updatedAt) || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<!-- Git Repository Tab -->
|
|
<TabPanel value="1">
|
|
<div v-if="gitRepositories.length === 0" class="text-center py-8 text-500">
|
|
<i class="pi pi-github text-6xl mb-3"></i>
|
|
<p>Keine Git-Repositories verknüpft</p>
|
|
<p class="text-sm">Füge ein Git-Repository hinzu, um Commits und Contributions zu analysieren.</p>
|
|
</div>
|
|
|
|
<div v-else class="flex flex-col gap-4">
|
|
<!-- Repository Selector -->
|
|
<div class="flex gap-3 align-items-center flex-wrap">
|
|
<label class="font-medium">Repository:</label>
|
|
<Select
|
|
v-model="selectedRepository"
|
|
:options="gitRepositories"
|
|
option-label="name"
|
|
placeholder="Repository auswählen"
|
|
class="flex-1"
|
|
/>
|
|
<label v-if="selectedRepository" class="font-medium">Branch:</label>
|
|
<InputText
|
|
v-if="selectedRepository"
|
|
v-model="selectedBranch"
|
|
placeholder="Branch (z.B. main)"
|
|
class="w-15rem"
|
|
/>
|
|
<label v-if="selectedRepository" class="font-medium">Jahr:</label>
|
|
<InputNumber
|
|
v-if="selectedRepository"
|
|
v-model="selectedYear"
|
|
:min="2000"
|
|
:max="2100"
|
|
:use-grouping="false"
|
|
class="w-10rem"
|
|
/>
|
|
<Button
|
|
v-if="selectedRepository"
|
|
icon="pi pi-refresh"
|
|
label="Cache aktualisieren"
|
|
size="small"
|
|
outlined
|
|
:loading="refreshingCache"
|
|
@click="refreshRepositoryCache"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Contribution Chart -->
|
|
<div v-if="selectedRepository" class="p-4 border-round border-1 surface-border">
|
|
<GitContributionChart
|
|
:repository-id="selectedRepository.id"
|
|
:branch="selectedBranch"
|
|
:year="selectedYear"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Commit List -->
|
|
<div v-if="selectedRepository" class="p-4 border-round border-1 surface-border">
|
|
<div class="font-semibold text-lg mb-3">Commit-Historie</div>
|
|
<GitCommitList
|
|
:repository-id="selectedRepository.id"
|
|
:branch="selectedBranch"
|
|
:limit="50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
|
|
<template #footer>
|
|
<Button label="Schließen" @click="viewDialog = false" />
|
|
<Button v-if="canEdit" label="Bearbeiten" @click="editFromView" icon="pi pi-pencil" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import CrudDataTable from '../components/CrudDataTable.vue'
|
|
import Dialog from 'primevue/dialog'
|
|
import Button from 'primevue/button'
|
|
import InputText from 'primevue/inputtext'
|
|
import Textarea from 'primevue/textarea'
|
|
import Select from 'primevue/select'
|
|
import DatePicker from 'primevue/datepicker'
|
|
import InputNumber from 'primevue/inputnumber'
|
|
import RadioButton from 'primevue/radiobutton'
|
|
import Tag from 'primevue/tag'
|
|
import Tabs from 'primevue/tabs'
|
|
import TabList from 'primevue/tablist'
|
|
import Tab from 'primevue/tab'
|
|
import TabPanels from 'primevue/tabpanels'
|
|
import TabPanel from 'primevue/tabpanel'
|
|
import DataTable from 'primevue/datatable'
|
|
import Column from 'primevue/column'
|
|
import GitCommitList from '../components/GitCommitList.vue'
|
|
import GitContributionChart from '../components/GitContributionChart.vue'
|
|
import DocumentUpload from '../components/DocumentUpload.vue'
|
|
|
|
const toast = useToast()
|
|
const tableRef = ref(null)
|
|
const projectDialog = ref(false)
|
|
const viewDialog = ref(false)
|
|
const deleteDialog = ref(false)
|
|
const editingProject = ref({})
|
|
const viewingProject = ref({})
|
|
const projectToDelete = ref(null)
|
|
const submitted = ref(false)
|
|
const saving = ref(false)
|
|
const deleting = ref(false)
|
|
const typeFilter = ref('all')
|
|
const customers = ref([])
|
|
const statuses = ref([])
|
|
const gitRepositories = ref([])
|
|
const selectedRepository = ref(null)
|
|
const selectedBranch = ref('main')
|
|
const selectedYear = ref(new Date().getFullYear())
|
|
const refreshingCache = ref(false)
|
|
|
|
// Git Repository Management
|
|
const editGitRepositories = ref([])
|
|
const gitRepoDialog = ref(false)
|
|
const editingGitRepo = ref({})
|
|
const gitRepoSubmitted = ref(false)
|
|
const savingGitRepo = ref(false)
|
|
const deleteGitRepoDialog = ref(false)
|
|
const gitRepoToDelete = ref(null)
|
|
const deletingGitRepo = ref(false)
|
|
const testingConnection = ref(false)
|
|
const connectionTestResult = ref(null)
|
|
|
|
const gitProviders = [
|
|
{ label: 'GitHub', value: 'github' },
|
|
{ label: 'Gitea', value: 'gitea' },
|
|
{ label: 'Lokal', value: 'local' }
|
|
]
|
|
|
|
// Permission checks (will be replaced with actual permission checks)
|
|
const canView = computed(() => true)
|
|
const canEdit = computed(() => true)
|
|
const canDelete = computed(() => true)
|
|
const canExport = computed(() => true)
|
|
|
|
// Column definitions
|
|
const projectColumns = ref([
|
|
{ key: 'name', label: 'Projektname', field: 'name', sortable: true, visible: true },
|
|
{ key: 'projectNumber', label: 'Projektnummer', field: 'projectNumber', sortable: true, visible: true },
|
|
{ key: 'customer', label: 'Kunde', field: 'customer', sortable: false, visible: true },
|
|
{ key: 'status', label: 'Status', field: 'status', sortable: false, visible: true },
|
|
{ key: 'orderNumber', label: 'Bestellnummer', field: 'orderNumber', sortable: true, visible: false },
|
|
{ key: 'orderDate', label: 'Bestelldatum', field: 'orderDate', sortable: true, visible: false },
|
|
{ key: 'startDate', label: 'Start', field: 'startDate', sortable: true, visible: true },
|
|
{ key: 'endDate', label: 'Ende', field: 'endDate', sortable: true, visible: true },
|
|
{ key: 'budget', label: 'Budget', field: 'budget', sortable: true, visible: true },
|
|
{ key: 'hourContingent', label: 'Stunden', field: 'hourContingent', sortable: true, visible: false },
|
|
{ key: 'type', label: 'Typ', field: 'type', sortable: false, visible: true },
|
|
{ key: 'createdAt', label: 'Erstellt am', field: 'createdAt', sortable: true, visible: false },
|
|
{ key: 'updatedAt', label: 'Geändert am', field: 'updatedAt', sortable: true, visible: false }
|
|
])
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadCustomers(), loadStatuses()])
|
|
})
|
|
|
|
async function loadCustomers() {
|
|
try {
|
|
const response = await fetch('/api/contacts?pagination=false')
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Kunden')
|
|
|
|
const data = await response.json()
|
|
const customersList = data['hydra:member'] || data.member || data
|
|
|
|
// Ensure it's an array
|
|
customers.value = Array.isArray(customersList) ? customersList : []
|
|
} catch (error) {
|
|
console.error('Error loading customers:', error)
|
|
customers.value = []
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Kunden konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
async function loadStatuses() {
|
|
try {
|
|
const response = await fetch('/api/project_statuses?pagination=false&isActive=true')
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Status')
|
|
|
|
const data = await response.json()
|
|
const statusesList = data['hydra:member'] || data.member || data
|
|
|
|
// Ensure it's an array
|
|
statuses.value = Array.isArray(statusesList) ? statusesList : []
|
|
} catch (error) {
|
|
console.error('Error loading statuses:', error)
|
|
statuses.value = []
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Status konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
function filterByType(type, loadData) {
|
|
typeFilter.value = type
|
|
|
|
let filters = {}
|
|
if (type === 'business') {
|
|
filters.isPrivate = false
|
|
} else if (type === 'private') {
|
|
filters.isPrivate = true
|
|
}
|
|
|
|
loadData(filters)
|
|
}
|
|
|
|
function onDataLoaded(data) {
|
|
console.log('Projects loaded:', data.length)
|
|
}
|
|
|
|
async function viewProject(project) {
|
|
viewingProject.value = { ...project }
|
|
viewDialog.value = true
|
|
|
|
// Load Git repositories for this project
|
|
await loadGitRepositories(project.id)
|
|
}
|
|
|
|
async function loadGitRepositories(projectId) {
|
|
try {
|
|
const response = await fetch(`/api/git_repositories?project=${projectId}`)
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Git-Repositories')
|
|
|
|
const data = await response.json()
|
|
gitRepositories.value = data['hydra:member'] || data.member || data || []
|
|
|
|
// Select first repository by default
|
|
if (gitRepositories.value.length > 0) {
|
|
selectedRepository.value = gitRepositories.value[0]
|
|
selectedBranch.value = gitRepositories.value[0].branch || 'main'
|
|
} else {
|
|
selectedRepository.value = null
|
|
selectedBranch.value = 'main'
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading git repositories:', error)
|
|
gitRepositories.value = []
|
|
selectedRepository.value = null
|
|
}
|
|
}
|
|
|
|
function editFromView() {
|
|
viewDialog.value = false
|
|
editProject(viewingProject.value)
|
|
}
|
|
|
|
function openNewProjectDialog() {
|
|
// Find default status
|
|
const defaultStatus = statuses.value.find(s => s.isDefault) || statuses.value[0] || null
|
|
|
|
editingProject.value = {
|
|
name: '',
|
|
description: '',
|
|
customer: null,
|
|
status: defaultStatus,
|
|
projectNumber: '',
|
|
orderNumber: '',
|
|
orderDate: null,
|
|
startDate: null,
|
|
endDate: null,
|
|
budget: null,
|
|
hourContingent: null,
|
|
isPrivate: false
|
|
}
|
|
submitted.value = false
|
|
projectDialog.value = true
|
|
}
|
|
|
|
function editProject(project) {
|
|
// Convert date strings to Date objects for Calendar component
|
|
editingProject.value = {
|
|
...project,
|
|
orderDate: project.orderDate ? new Date(project.orderDate) : null,
|
|
startDate: project.startDate ? new Date(project.startDate) : null,
|
|
endDate: project.endDate ? new Date(project.endDate) : null,
|
|
budget: project.budget ? parseFloat(project.budget) : null,
|
|
hourContingent: project.hourContingent ? parseFloat(project.hourContingent) : null
|
|
}
|
|
submitted.value = false
|
|
projectDialog.value = true
|
|
|
|
// Load git repositories for editing if project exists
|
|
if (project.id) {
|
|
loadGitRepositoriesForEdit(project.id)
|
|
}
|
|
}
|
|
|
|
async function loadGitRepositoriesForEdit(projectId) {
|
|
try {
|
|
const response = await fetch(`/api/git_repositories?project=${projectId}`)
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Git-Repositories')
|
|
|
|
const data = await response.json()
|
|
editGitRepositories.value = data['hydra:member'] || data.member || data || []
|
|
} catch (error) {
|
|
console.error('Error loading git repositories for edit:', error)
|
|
editGitRepositories.value = []
|
|
}
|
|
}
|
|
|
|
// Git Repository Management Functions
|
|
function openAddGitRepoDialog() {
|
|
editingGitRepo.value = {
|
|
name: '',
|
|
url: '',
|
|
provider: null,
|
|
branch: 'main',
|
|
localPath: '',
|
|
accessToken: '',
|
|
description: ''
|
|
}
|
|
gitRepoSubmitted.value = false
|
|
connectionTestResult.value = null
|
|
gitRepoDialog.value = true
|
|
}
|
|
|
|
function editGitRepo(repo) {
|
|
editingGitRepo.value = { ...repo }
|
|
gitRepoSubmitted.value = false
|
|
connectionTestResult.value = null
|
|
gitRepoDialog.value = true
|
|
}
|
|
|
|
async function testRepositoryConnection() {
|
|
if (!editingGitRepo.value.url || !editingGitRepo.value.provider) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Hinweis',
|
|
detail: 'Bitte URL und Provider angeben',
|
|
life: 3000
|
|
})
|
|
return
|
|
}
|
|
|
|
testingConnection.value = true
|
|
connectionTestResult.value = null
|
|
|
|
try {
|
|
// Create a temporary repository object for testing
|
|
const testData = {
|
|
url: editingGitRepo.value.url,
|
|
provider: editingGitRepo.value.provider,
|
|
branch: editingGitRepo.value.branch || 'main',
|
|
accessToken: editingGitRepo.value.accessToken || null,
|
|
localPath: editingGitRepo.value.localPath || null
|
|
}
|
|
|
|
const response = await fetch('/api/git-repos/test', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(testData)
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
console.log('Test response:', response.status, result)
|
|
|
|
if (response.ok && result.success) {
|
|
connectionTestResult.value = {
|
|
success: true,
|
|
message: result.message || 'Repository ist erreichbar und korrekt konfiguriert',
|
|
details: result.details || {}
|
|
}
|
|
|
|
// Auto-fill branch if detected
|
|
if (result.details?.defaultBranch && !editingGitRepo.value.branch) {
|
|
editingGitRepo.value.branch = result.details.defaultBranch
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Repository-Verbindung erfolgreich getestet',
|
|
life: 3000
|
|
})
|
|
} else {
|
|
connectionTestResult.value = {
|
|
success: false,
|
|
message: result.message || 'Verbindung zum Repository fehlgeschlagen',
|
|
error: result.error || ''
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: result.message || 'Verbindung fehlgeschlagen',
|
|
life: 5000
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('Error testing repository connection:', error)
|
|
connectionTestResult.value = {
|
|
success: false,
|
|
message: 'Netzwerkfehler beim Testen der Verbindung',
|
|
error: error.message
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Verbindung konnte nicht getestet werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
testingConnection.value = false
|
|
}
|
|
}
|
|
|
|
async function saveGitRepo() {
|
|
gitRepoSubmitted.value = true
|
|
|
|
if (!editingGitRepo.value.name || !editingGitRepo.value.url) {
|
|
return
|
|
}
|
|
|
|
savingGitRepo.value = true
|
|
|
|
try {
|
|
const repoData = {
|
|
name: editingGitRepo.value.name,
|
|
url: editingGitRepo.value.url,
|
|
provider: editingGitRepo.value.provider || null,
|
|
branch: editingGitRepo.value.branch || 'main',
|
|
localPath: editingGitRepo.value.localPath || null,
|
|
accessToken: editingGitRepo.value.accessToken || null,
|
|
description: editingGitRepo.value.description || null,
|
|
project: `/api/projects/${editingProject.value.id}`
|
|
}
|
|
|
|
const isNew = !editingGitRepo.value.id
|
|
const url = isNew ? '/api/git_repositories' : `/api/git_repositories/${editingGitRepo.value.id}`
|
|
const method = isNew ? 'POST' : 'PUT'
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(repoData)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.message || 'Fehler beim Speichern des Repositories')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: isNew ? 'Repository hinzugefügt' : 'Repository aktualisiert',
|
|
life: 3000
|
|
})
|
|
|
|
gitRepoDialog.value = false
|
|
|
|
// Reload git repositories
|
|
await loadGitRepositoriesForEdit(editingProject.value.id)
|
|
} catch (error) {
|
|
console.error('Error saving git repository:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Repository konnte nicht gespeichert werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
savingGitRepo.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDeleteGitRepo(repo) {
|
|
gitRepoToDelete.value = repo
|
|
deleteGitRepoDialog.value = true
|
|
}
|
|
|
|
async function deleteGitRepo() {
|
|
if (!gitRepoToDelete.value) return
|
|
|
|
deletingGitRepo.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/git_repositories/${gitRepoToDelete.value.id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Löschen des Repositories')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Repository entfernt',
|
|
life: 3000
|
|
})
|
|
|
|
deleteGitRepoDialog.value = false
|
|
|
|
// Reload git repositories
|
|
await loadGitRepositoriesForEdit(editingProject.value.id)
|
|
} catch (error) {
|
|
console.error('Error deleting git repository:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Repository konnte nicht entfernt werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
deletingGitRepo.value = false
|
|
}
|
|
}
|
|
|
|
async function saveProject() {
|
|
submitted.value = true
|
|
|
|
if (!editingProject.value.name) {
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
// Prepare data for API
|
|
const projectData = {
|
|
name: editingProject.value.name,
|
|
description: editingProject.value.description || null,
|
|
customer: editingProject.value.customer ? `/api/contacts/${editingProject.value.customer.id}` : null,
|
|
status: editingProject.value.status ? `/api/project_statuses/${editingProject.value.status.id}` : null,
|
|
projectNumber: editingProject.value.projectNumber || null,
|
|
orderNumber: editingProject.value.orderNumber || null,
|
|
orderDate: editingProject.value.orderDate ? formatDateForAPI(editingProject.value.orderDate) : null,
|
|
startDate: editingProject.value.startDate ? formatDateForAPI(editingProject.value.startDate) : null,
|
|
endDate: editingProject.value.endDate ? formatDateForAPI(editingProject.value.endDate) : null,
|
|
budget: editingProject.value.budget ? editingProject.value.budget.toString() : null,
|
|
hourContingent: editingProject.value.hourContingent ? editingProject.value.hourContingent.toString() : null,
|
|
isPrivate: editingProject.value.isPrivate
|
|
}
|
|
|
|
const isNew = !editingProject.value.id
|
|
const url = isNew ? '/api/projects' : `/api/projects/${editingProject.value.id}`
|
|
const method = isNew ? 'POST' : 'PUT'
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/ld+json',
|
|
'Accept': 'application/ld+json'
|
|
},
|
|
body: JSON.stringify(projectData)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error['hydra:description'] || 'Fehler beim Speichern')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: `Projekt wurde ${isNew ? 'erstellt' : 'aktualisiert'}`,
|
|
life: 3000
|
|
})
|
|
|
|
projectDialog.value = false
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error saving project:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Projekt konnte nicht gespeichert werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete(project) {
|
|
projectToDelete.value = project
|
|
deleteDialog.value = true
|
|
}
|
|
|
|
async function deleteProject() {
|
|
deleting.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectToDelete.value.id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Löschen')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Projekt wurde gelöscht',
|
|
life: 3000
|
|
})
|
|
|
|
deleteDialog.value = false
|
|
projectToDelete.value = null
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error deleting project:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Projekt konnte nicht gelöscht werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
|
|
async function refreshRepositoryCache() {
|
|
if (!selectedRepository.value) return
|
|
|
|
refreshingCache.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/git-repos/${selectedRepository.value.id}/refresh-cache`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.error || 'Cache konnte nicht aktualisiert werden')
|
|
}
|
|
|
|
const result = await response.json()
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: result.message || 'Cache wurde aktualisiert',
|
|
life: 3000
|
|
})
|
|
|
|
// Reload git data
|
|
await loadGitRepositories(viewingProject.value.id)
|
|
} catch (error) {
|
|
console.error('Error refreshing cache:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Cache konnte nicht aktualisiert werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
refreshingCache.value = false
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return ''
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
function formatDateForAPI(date) {
|
|
if (!date) return null
|
|
const d = new Date(date)
|
|
const year = d.getFullYear()
|
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
function formatCurrency(value) {
|
|
if (!value) return ''
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(value)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.project-management {
|
|
height: 100%;
|
|
}
|
|
</style>
|