702 lines
45 KiB
JavaScript
702 lines
45 KiB
JavaScript
Vue.component('warehouse-project', {
|
|
template: `
|
|
<tt-card>
|
|
<div class="mb-3">
|
|
<button @click="openProjectModal('create')" class="btn btn-primary shadow-sm"><i class="fas fa-plus"></i> Projekt erstellen</button>
|
|
</div>
|
|
|
|
<tt-table-crud emit-edit
|
|
ref="table"
|
|
:additional-actions="additionalActions"
|
|
@edit="openProjectModal($event.id)">
|
|
|
|
<template v-slot:expandedRow="{ row }">
|
|
<warehouse-project-details :project="row" @refresh="$refs.table.$refs.table.refreshTable()"/>
|
|
</template>
|
|
|
|
</tt-table-crud>
|
|
|
|
<warehouse-project-modal
|
|
v-if="projectModalId"
|
|
:id="projectModalId"
|
|
@close="closeProjectModal"
|
|
/>
|
|
</tt-card>
|
|
`,
|
|
data() {
|
|
return {
|
|
additionalActions: [],
|
|
projectModalId: null
|
|
};
|
|
},
|
|
methods: {
|
|
openProjectModal(id) {
|
|
this.projectModalId = id;
|
|
},
|
|
closeProjectModal() {
|
|
this.projectModalId = null;
|
|
this.$refs.table.$refs.table.refreshTable();
|
|
}
|
|
}
|
|
});
|
|
|
|
Vue.component('warehouse-project-details', {
|
|
props: ['project'],
|
|
template: `
|
|
<div class="my-3 bg-white p-1" style="cursor: default;">
|
|
<!-- Tabs Header -->
|
|
<div class="card border-0 shadow-sm mb-3">
|
|
<div class="card-header bg-white border-0 p-2">
|
|
<ul class="nav nav-pills nav-fill">
|
|
<li class="nav-item">
|
|
<a class="nav-link font-weight-bold" :class="{active: activeTab === 'overview'}" href="#" @click.prevent="activeTab = 'overview'"><i class="fas fa-info-circle mr-1"></i> Übersicht</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link font-weight-bold" :class="{active: activeTab === 'tasks'}" href="#" @click.prevent="activeTab = 'tasks'"><i class="fas fa-tasks mr-1"></i> Aufgaben</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link font-weight-bold" :class="{active: activeTab === 'team'}" href="#" @click.prevent="activeTab = 'team'"><i class="fas fa-users mr-1"></i> Team</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link font-weight-bold" :class="{active: activeTab === 'logistics'}" href="#" @click.prevent="activeTab = 'logistics'"><i class="fas fa-truck-loading mr-1"></i> Logistik</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link font-weight-bold" :class="{active: activeTab === 'journal'}" href="#" @click.prevent="activeTab = 'journal'"><i class="fas fa-book mr-1"></i> Journal</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Content -->
|
|
<div class="">
|
|
<!-- OVERVIEW -->
|
|
<div v-if="activeTab === 'overview'" class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6 border-right">
|
|
<h5 class="card-title text-primary border-bottom pb-2 mb-3">Projektdaten</h5>
|
|
<dl class="row">
|
|
<dt class="col-sm-4 text-muted">Status</dt>
|
|
<dd class="col-sm-8">
|
|
<span :class="statusBadgeClass(project.status)">{{ statusText(project.status) }}</span>
|
|
</dd>
|
|
|
|
<dt class="col-sm-4 text-muted">Projekt-Nr.</dt>
|
|
<dd class="col-sm-8 font-weight-bold">{{ project.projectNumber }}</dd>
|
|
|
|
<dt class="col-sm-4 text-muted">Zeitraum</dt>
|
|
<dd class="col-sm-8">
|
|
<i class="far fa-calendar-alt text-info mr-1"></i> {{ formatDate(project.startDate) }} - {{ formatDate(project.endDate) }}
|
|
</dd>
|
|
|
|
<dt class="col-sm-4 text-muted">Lagerort</dt>
|
|
<dd class="col-sm-8">{{ project.storageLocation || '-' }}</dd>
|
|
|
|
<dt class="col-sm-4 text-muted">Budget</dt>
|
|
<dd class="col-sm-8 font-weight-bold text-success">{{ formatPrice(project.financials) }}</dd>
|
|
</dl>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h5 class="card-title text-primary border-bottom pb-2 mb-3">Details</h5>
|
|
<p class="text-uppercase text-secondary small font-weight-bold mb-1">Beschreibung</p>
|
|
<div class="p-3 bg-light rounded mb-4 border" style="min-height: 80px;">{{ project.description || 'Keine Beschreibung vorhanden.' }}</div>
|
|
|
|
<p class="text-uppercase text-secondary small font-weight-bold mb-1">Externes Team / Partner</p>
|
|
<div class="p-3 bg-light rounded border">{{ project.externalTeam || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TASKS -->
|
|
<div v-if="activeTab === 'tasks'" class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 bg-light p-3 rounded border">
|
|
<div class="d-flex align-items-center flex-grow-1 mr-4">
|
|
<span class="mr-3 font-weight-bold text-muted">Fortschritt:</span>
|
|
<div class="progress flex-grow-1 shadow-sm" style="height: 25px;">
|
|
<div class="progress-bar bg-success progress-bar-striped progress-bar-animated" role="progressbar" :style="{width: taskProgress + '%'}" :aria-valuenow="taskProgress" aria-valuemin="0" aria-valuemax="100">
|
|
{{ taskProgress }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-success shadow" @click="openTaskModal()"><i class="fas fa-plus-circle"></i> Neue Aufgabe</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- TODO Column -->
|
|
<div class="col-md-4">
|
|
<div class="bg-secondary-light p-3 rounded h-100 border shadow-sm" style="background-color: #f8f9fa;">
|
|
<h6 class="text-uppercase text-secondary font-weight-bold text-center mb-3 border-bottom pb-2">
|
|
<i class="far fa-circle mr-1"></i> Zu Erledigen
|
|
<span class="badge badge-secondary badge-pill ml-1">{{ tasksByStatus('todo').length }}</span>
|
|
</h6>
|
|
<div v-for="task in tasksByStatus('todo')" :key="task.id" class="card mb-3 shadow-sm task-card border-left-warning" style="border-left-width: 5px;">
|
|
<div class="card-body p-3">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<h6 class="card-title mb-1 font-weight-bold text-dark">{{ task.title }}</h6>
|
|
<div class="dropdown">
|
|
<button class="btn btn-link btn-sm p-0 text-muted" type="button" data-toggle="dropdown"><i class="fas fa-ellipsis-v"></i></button>
|
|
<div class="dropdown-menu dropdown-menu-right shadow">
|
|
<a class="dropdown-item" href="#" @click.prevent="openTaskModal(task)"><i class="fas fa-edit mr-2 text-info"></i> Bearbeiten</a>
|
|
<a class="dropdown-item" href="#" @click.prevent="updateTaskStatus(task.id, 'in_progress')"><i class="fas fa-play mr-2 text-primary"></i> Starten</a>
|
|
<div class="dropdown-divider"></div>
|
|
<a class="dropdown-item text-danger" href="#" @click.prevent="deleteTask(task.id)"><i class="fas fa-trash-alt mr-2"></i> Löschen</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="card-text small text-muted mb-2 mt-1" v-if="task.description">{{ task.description }}</p>
|
|
<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
|
|
<small class="text-muted"><i class="fas fa-user-circle mr-1"></i> {{ task.assignedUserName || 'Niemand' }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="tasksByStatus('todo').length === 0" class="text-center text-muted small mt-5 p-4 border border-dashed rounded">Keine Aufgaben</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- IN PROGRESS Column -->
|
|
<div class="col-md-4">
|
|
<div class="bg-primary-light p-3 rounded h-100 border shadow-sm" style="background-color: #eef5ff;">
|
|
<h6 class="text-uppercase text-primary font-weight-bold text-center mb-3 border-bottom pb-2">
|
|
<i class="fas fa-spinner fa-spin mr-1"></i> In Arbeit
|
|
<span class="badge badge-primary badge-pill ml-1">{{ tasksByStatus('in_progress').length }}</span>
|
|
</h6>
|
|
<div v-for="task in tasksByStatus('in_progress')" :key="task.id" class="card mb-3 shadow-sm task-card border-left-primary" style="border-left-width: 5px;">
|
|
<div class="card-body p-3">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<h6 class="card-title mb-1 font-weight-bold text-dark">{{ task.title }}</h6>
|
|
<div class="dropdown">
|
|
<button class="btn btn-link btn-sm p-0 text-muted" type="button" data-toggle="dropdown"><i class="fas fa-ellipsis-v"></i></button>
|
|
<div class="dropdown-menu dropdown-menu-right shadow">
|
|
<a class="dropdown-item" href="#" @click.prevent="openTaskModal(task)"><i class="fas fa-edit mr-2 text-info"></i> Bearbeiten</a>
|
|
<a class="dropdown-item" href="#" @click.prevent="updateTaskStatus(task.id, 'done')"><i class="fas fa-check mr-2 text-success"></i> Erledigen</a>
|
|
<a class="dropdown-item" href="#" @click.prevent="updateTaskStatus(task.id, 'todo')"><i class="fas fa-undo mr-2 text-secondary"></i> Zurückstellen</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="card-text small text-muted mb-2 mt-1" v-if="task.description">{{ task.description }}</p>
|
|
<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
|
|
<small class="text-primary font-weight-bold"><i class="fas fa-user-circle mr-1"></i> {{ task.assignedUserName || 'Niemand' }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DONE Column -->
|
|
<div class="col-md-4">
|
|
<div class="bg-success-light p-3 rounded h-100 border shadow-sm" style="background-color: #e8f5e9;">
|
|
<h6 class="text-uppercase text-success font-weight-bold text-center mb-3 border-bottom pb-2">
|
|
<i class="fas fa-check-circle mr-1"></i> Erledigt
|
|
<span class="badge badge-success badge-pill ml-1">{{ tasksByStatus('done').length }}</span>
|
|
</h6>
|
|
<div v-for="task in tasksByStatus('done')" :key="task.id" class="card mb-3 shadow-sm task-card border-left-success" style="border-left-width: 5px; opacity: 0.85;">
|
|
<div class="card-body p-3">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<h6 class="card-title mb-1 font-weight-bold text-dark"><del>{{ task.title }}</del></h6>
|
|
<div class="dropdown">
|
|
<button class="btn btn-link btn-sm p-0 text-muted" type="button" data-toggle="dropdown"><i class="fas fa-ellipsis-v"></i></button>
|
|
<div class="dropdown-menu dropdown-menu-right shadow">
|
|
<a class="dropdown-item" href="#" @click.prevent="updateTaskStatus(task.id, 'in_progress')"><i class="fas fa-undo mr-2 text-primary"></i> Wieder öffnen</a>
|
|
<a class="dropdown-item text-danger" href="#" @click.prevent="deleteTask(task.id)"><i class="fas fa-trash-alt mr-2"></i> Löschen</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
|
|
<small class="text-success"><i class="fas fa-check mr-1"></i> Abgeschlossen</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TEAM -->
|
|
<div v-if="activeTab === 'team'" class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card shadow border-0">
|
|
<div class="card-header bg-white text-dark d-flex justify-content-between align-items-center border-bottom">
|
|
<h5 class="mb-0 font-weight-bold"><i class="fas fa-users mr-2 text-primary"></i> Projektteam</h5>
|
|
<span class="badge badge-pill badge-primary">{{ team.length }} Mitglieder</span>
|
|
</div>
|
|
<div class="card-body bg-light">
|
|
<div class="input-group mb-4 shadow-sm bg-white rounded">
|
|
<tt-select
|
|
v-model="selectedUserId"
|
|
:options="allUsersSelectItems"
|
|
placeholder="Mitarbeiter auswählen"
|
|
class="form-control border-0"
|
|
style="flex: 1;"
|
|
></tt-select>
|
|
<div class="input-group-append">
|
|
<button class="btn btn-success font-weight-bold" @click="addMember" :disabled="!selectedUserId"><i class="fas fa-user-plus mr-1"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<ul class="list-group list-group-flush rounded shadow-sm">
|
|
<li v-for="member in team" :key="member.memberId" class="list-group-item d-flex justify-content-between align-items-center border-bottom hover-bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-circle bg-gradient-primary text-white mr-3 d-flex align-items-center justify-content-center shadow-sm"
|
|
style="width: 45px; height: 45px; border-radius: 50%; font-weight: bold; font-size: 1.1rem; background: linear-gradient(45deg, #007bff, #0056b3);">
|
|
{{ member.name.charAt(0) }}
|
|
</div>
|
|
<div>
|
|
<h6 class="mb-0 font-weight-bold text-dark">{{ member.name }}</h6>
|
|
<small class="text-muted"><i class="fas fa-id-badge mr-1"></i> {{ member.role || 'Projektmitarbeiter' }}</small>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-outline-danger btn-sm rounded-circle shadow-sm" @click="removeMember(member.memberId)" title="Entfernen" style="width: 32px; height: 32px;"><i class="fas fa-times"></i></button>
|
|
</li>
|
|
<li v-if="team.length === 0" class="list-group-item text-center text-muted py-5">
|
|
<div class="opacity-50">
|
|
<i class="fas fa-users fa-4x mb-3 text-gray-300"></i>
|
|
<p class="lead">Noch keine Teammitglieder zugewiesen.</p>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOGISTICS -->
|
|
<div v-if="activeTab === 'logistics'" class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 bg-light p-3 rounded border shadow-sm">
|
|
<h5 class="mb-0 text-dark"><i class="fas fa-boxes mr-2"></i> Logistik & Bestellungen</h5>
|
|
<button class="btn btn-primary shadow" @click="openLinkOrderModal"><i class="fas fa-link mr-1"></i> Bestellwunsch verknüpfen</button>
|
|
</div>
|
|
|
|
<div class="accordion" id="logisticsAccordion">
|
|
<div v-for="request in linkedOrders" :key="request.linkId" class="card mb-3 shadow-sm border-0">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center pointer border rounded"
|
|
:id="'heading' + request.linkId"
|
|
data-toggle="collapse"
|
|
:data-target="'#collapse' + request.linkId"
|
|
aria-expanded="true"
|
|
:aria-controls="'collapse' + request.linkId"
|
|
style="transition: background-color 0.2s;">
|
|
<div class="d-flex align-items-center">
|
|
<span class="fa-stack mr-2 text-primary">
|
|
<i class="fas fa-circle fa-stack-2x opacity-25"></i>
|
|
<i class="fas fa-hashtag fa-stack-1x"></i>
|
|
</span>
|
|
<div>
|
|
<span class="font-weight-bold text-dark">{{ request.requestId }}</span>
|
|
<span class="mx-2 text-muted">|</span>
|
|
<span class="font-weight-500">{{ request.purpose }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<span v-if="request.status === 'done'" class="badge badge-success px-3 py-2 shadow-sm"><i class="fas fa-check mr-1"></i> Erledigt</span>
|
|
<span v-else-if="request.status === 'cancelled'" class="badge badge-danger px-3 py-2 shadow-sm"><i class="fas fa-ban mr-1"></i> Storniert</span>
|
|
<span v-else class="badge badge-info px-3 py-2 shadow-sm"><i class="fas fa-clock mr-1"></i> Offen</span>
|
|
|
|
<button class="btn btn-sm btn-outline-danger ml-3 rounded-circle shadow-sm" style="width: 30px; height: 30px;" @click.stop="unlinkOrder(request.linkId)" title="Verknüpfung lösen"><i class="fas fa-unlink"></i></button>
|
|
<i class="fas fa-chevron-down ml-3 text-muted"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div :id="'collapse' + request.linkId" class="collapse show" :aria-labelledby="'heading' + request.linkId">
|
|
<div class="card-body bg-light border-left border-right border-bottom p-0">
|
|
<div class="p-3">
|
|
<h6 class="text-muted text-uppercase small font-weight-bold mb-3 pl-2 border-left-primary" style="border-left: 3px solid #007bff;">Enthaltene Bestellungen</h6>
|
|
<div v-if="request.orders && request.orders.length > 0">
|
|
<div class="table-responsive bg-white rounded shadow-sm border">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="thead-light">
|
|
<tr>
|
|
<th class="border-top-0">Order Nr.</th>
|
|
<th class="border-top-0">Status</th>
|
|
<th class="border-top-0 text-right">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="order in request.orders" :key="order.id">
|
|
<td class="align-middle"><a :href="'/WarehouseOrder?id=' + order.id" target="_blank" class="font-weight-bold text-primary"><i class="fas fa-file-invoice mr-1"></i> {{ order.orderNumber }}</a></td>
|
|
<td class="align-middle">
|
|
<span class="badge badge-light border text-secondary px-2 py-1">{{ order.status || 'Unbekannt' }}</span>
|
|
</td>
|
|
<td class="align-middle text-right">
|
|
<a :href="'/WarehouseOrder?id=' + order.id" target="_blank" class="btn btn-sm btn-light border shadow-sm"><i class="fas fa-external-link-alt text-secondary"></i> Öffnen</a>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-muted small font-italic p-3 text-center">
|
|
Keine direkten Bestellungen verknüpft oder Daten noch nicht verfügbar.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="linkedOrders.length === 0" class="text-center text-muted py-5 border rounded bg-light shadow-sm">
|
|
<i class="fas fa-truck-loading fa-4x mb-3 text-gray-300"></i>
|
|
<p class="h5 font-weight-light">Keine verknüpften Bestellwünsche.</p>
|
|
<button class="btn btn-outline-primary mt-2 btn-sm" @click="openLinkOrderModal">Jetzt verknüpfen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JOURNAL -->
|
|
<div v-if="activeTab === 'journal'" class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<!-- Stylish Input Area -->
|
|
<div class="card shadow-sm mb-3 border-0 bg-light">
|
|
<div class="card-body p-2">
|
|
<div class="d-flex align-items-start">
|
|
<div class="avatar-circle bg-primary text-white mr-3 d-flex align-items-center justify-content-center rounded-circle mt-1 shadow-sm" style="width: 35px; height: 35px; font-size: 0.9rem;">
|
|
<i class="fas fa-pen"></i>
|
|
</div>
|
|
<div class="flex-grow-1 position-relative">
|
|
<textarea v-model="newJournalMessage"
|
|
class="form-control border-0 bg-white shadow-sm"
|
|
rows="2"
|
|
style="resize: none; border-radius: 1rem; padding: 10px; font-size: 0.9rem;"
|
|
placeholder="Schreiben Sie eine Notiz..."></textarea>
|
|
<button class="btn btn-primary rounded-circle shadow position-absolute"
|
|
style="bottom: -10px; right: 10px; width: 35px; height: 35px; display: flex; align-items: center; justify-content: center;"
|
|
@click="postJournal"
|
|
:disabled="!newJournalMessage.trim()">
|
|
<i class="fas fa-paper-plane small"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scrollable Feed -->
|
|
<div class="journal-feed pr-2" style="max-height: 500px; overflow-y: auto;">
|
|
<div v-for="log in journal" :key="log.id" class="media mb-3 p-2 bg-white shadow-sm rounded border-left-info position-relative" style="border-left: 3px solid #17a2b8;">
|
|
<div class="mr-2 text-center" style="width: 45px;">
|
|
<div class="text-muted small font-weight-bold" style="font-size: 0.75rem;">{{ new Date(log.create * 1000).toLocaleDateString('de-DE', {day: '2-digit', month: '2-digit'}) }}</div>
|
|
<div class="text-muted small" style="font-size: 0.7rem;">{{ new Date(log.create * 1000).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}) }}</div>
|
|
</div>
|
|
<div class="media-body">
|
|
<h6 class="mt-0 mb-1 font-weight-bold text-dark d-flex justify-content-between" style="font-size: 0.9rem;">
|
|
{{ log.userName }}
|
|
</h6>
|
|
<div class="text-secondary" style="white-space: pre-wrap; font-size: 0.85rem; line-height: 1.4;">{{ log.text }}</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="journal.length === 0" class="text-center text-muted py-4 opacity-50">
|
|
<i class="fas fa-history fa-2x mb-2 text-gray-300"></i>
|
|
<p class="small">Noch keine Journal-Einträge.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card shadow border-0">
|
|
<div class="card-header bg-secondary text-white py-2">
|
|
<h6 class="mb-0 small"><i class="fas fa-paperclip mr-1"></i> Dokumente</h6>
|
|
</div>
|
|
<div class="card-body text-center text-muted bg-light" style="min-height: 150px; display: flex; flex-direction: column; justify-content: center;">
|
|
<i class="fas fa-cloud-upload-alt fa-2x mb-2 text-gray-400"></i>
|
|
<p class="small mb-0">Dateien hierher ziehen</p>
|
|
<p class="small text-muted font-italic" style="font-size: 0.7rem;">(WIP: Upload Funktion)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Task Modal -->
|
|
<div class="modal fade" id="taskModal" tabindex="-1" role="dialog" ref="taskModal">
|
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
<div class="modal-content border-0 shadow-lg">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title font-weight-bold"><i class="fas fa-tasks mr-2"></i> {{ currentTask.id ? 'Aufgabe bearbeiten' : 'Neue Aufgabe' }}</h5>
|
|
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body bg-light">
|
|
<form @submit.prevent="saveTask">
|
|
<div class="form-group">
|
|
<label class="font-weight-bold text-dark">Titel <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control shadow-sm" v-model="currentTask.title" required placeholder="Was muss erledigt werden?">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="font-weight-bold text-dark">Beschreibung</label>
|
|
<textarea class="form-control shadow-sm" v-model="currentTask.description" rows="4" placeholder="Details zur Aufgabe..."></textarea>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="font-weight-bold text-dark">Zugewiesen an</label>
|
|
<select class="form-control shadow-sm" v-model="currentTask.assignedUserId">
|
|
<option value="">-- Unzugewiesen --</option>
|
|
<option v-for="u in allUsers" :key="u.id" :value="u.id">{{ u.name }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="font-weight-bold text-dark">Status</label>
|
|
<select class="form-control shadow-sm" v-model="currentTask.status">
|
|
<option value="todo">ToDo</option>
|
|
<option value="in_progress">In Arbeit</option>
|
|
<option value="done">Erledigt</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer bg-white">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary shadow" @click="saveTask">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Link Order Modal -->
|
|
<div class="modal fade" id="linkOrderModal" tabindex="-1" role="dialog" ref="linkOrderModal">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
|
<div class="modal-content border-0 shadow-lg">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title font-weight-bold"><i class="fas fa-link mr-2"></i> Bestellwunsch verknüpfen</h5>
|
|
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body bg-light" style="max-height: 60vh; overflow-y: auto;">
|
|
<div v-if="availableOrders.length === 0" class="alert alert-info shadow-sm text-center py-4">
|
|
<i class="fas fa-info-circle fa-2x mb-3"></i>
|
|
<p class="mb-0">Keine offenen Bestellwünsche gefunden.</p>
|
|
</div>
|
|
<div v-else class="list-group shadow-sm">
|
|
<button v-for="req in availableOrders" :key="req.id" type="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center p-3 hover-bg-light" @click="linkOrder(req.id)">
|
|
<div>
|
|
<h6 class="mb-1 font-weight-bold text-primary">#{{ req.id }} - {{ req.purpose }}</h6>
|
|
<small class="text-muted"><i class="far fa-clock mr-1"></i> Erstellt am: {{ formatDateTime(req.create) }}</small>
|
|
</div>
|
|
<i class="fas fa-plus-circle text-success fa-2x"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-white">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Schließen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
data() {
|
|
return {
|
|
activeTab: 'overview',
|
|
tasks: [],
|
|
team: [],
|
|
linkedOrders: [],
|
|
journal: [],
|
|
allUsers: [],
|
|
availableOrders: [],
|
|
selectedUserId: '',
|
|
newJournalMessage: '',
|
|
currentTask: {
|
|
id: null,
|
|
title: '',
|
|
description: '',
|
|
assignedUserId: '',
|
|
status: 'todo'
|
|
}
|
|
};
|
|
},
|
|
computed: {
|
|
taskProgress() {
|
|
if (!this.tasks || !this.tasks.length) return 0;
|
|
const done = this.tasks.filter(t => t.status === 'done').length;
|
|
return Math.round((done / this.tasks.length) * 100);
|
|
},
|
|
allUsersSelectItems() {
|
|
return this.allUsers.map(u => ({ value: u.id, text: u.name }));
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadData();
|
|
},
|
|
methods: {
|
|
async loadData() {
|
|
try {
|
|
await Promise.all([
|
|
this.fetchTasks(),
|
|
this.fetchTeam(),
|
|
this.fetchLinkedOrders(),
|
|
this.fetchJournal(),
|
|
this.fetchUsers()
|
|
]);
|
|
} catch (e) {
|
|
console.error("Error loading project data", e);
|
|
}
|
|
},
|
|
async fetchTasks() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getTasks?id=' + this.project.id);
|
|
this.tasks = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.tasks = [];
|
|
}
|
|
},
|
|
async fetchTeam() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getTeam?id=' + this.project.id);
|
|
this.team = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.team = [];
|
|
}
|
|
},
|
|
async fetchLinkedOrders() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getLinkedOrders?id=' + this.project.id);
|
|
this.linkedOrders = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.linkedOrders = [];
|
|
}
|
|
},
|
|
async fetchJournal() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getJournal?id=' + this.project.id);
|
|
this.journal = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.journal = [];
|
|
}
|
|
},
|
|
async fetchUsers() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getUsers');
|
|
this.allUsers = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.allUsers = [];
|
|
}
|
|
},
|
|
async fetchAvailableOrders() {
|
|
try {
|
|
const res = await axios.get('/WarehouseProject/getAvailableOrderRequests');
|
|
this.availableOrders = Array.isArray(res.data) ? res.data : [];
|
|
} catch(e) {
|
|
this.availableOrders = [];
|
|
}
|
|
},
|
|
// Helpers
|
|
tasksByStatus(status) {
|
|
if (!this.tasks) return [];
|
|
return this.tasks.filter(t => t.status === status);
|
|
},
|
|
statusLabel(status) {
|
|
const map = { todo: 'ToDo', in_progress: 'In Arbeit', done: 'Erledigt' };
|
|
return map[status] || status;
|
|
},
|
|
statusText(status) {
|
|
const map = { new: 'Neu', wip: 'In Bearbeitung', finished: 'Abgeschlossen', cancelled: 'Storniert' };
|
|
return map[status] || status;
|
|
},
|
|
statusBadgeClass(status) {
|
|
const map = { new: 'badge badge-primary', wip: 'badge badge-warning', finished: 'badge badge-success', cancelled: 'badge badge-danger' };
|
|
return map[status] || 'badge badge-secondary';
|
|
},
|
|
formatDate(ts) {
|
|
if (!ts) return '-';
|
|
return new Date(ts * 1000).toLocaleDateString('de-DE');
|
|
},
|
|
formatDateTime(ts) {
|
|
if (!ts) return '-';
|
|
return new Date(ts * 1000).toLocaleString('de-DE');
|
|
},
|
|
formatPrice(val) {
|
|
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val || 0);
|
|
},
|
|
// Actions
|
|
openTaskModal(task = null) {
|
|
if (task) {
|
|
this.currentTask = { ...task };
|
|
} else {
|
|
this.currentTask = { id: null, title: '', description: '', assignedUserId: '', status: 'todo' };
|
|
}
|
|
$(this.$refs.taskModal).modal('show');
|
|
},
|
|
async saveTask() {
|
|
if (!this.currentTask.title) return alert("Titel erforderlich");
|
|
|
|
try {
|
|
await axios.post('/WarehouseProject/saveTask', { ...this.currentTask, projectId: this.project.id });
|
|
$(this.$refs.taskModal).modal('hide');
|
|
this.fetchTasks();
|
|
this.fetchJournal();
|
|
} catch(e) {
|
|
console.error(e);
|
|
alert("Fehler beim Speichern: " + (e.response?.data?.error || 'Unbekannt'));
|
|
}
|
|
},
|
|
async updateTaskStatus(id, status) {
|
|
await axios.post('/WarehouseProject/updateTaskStatus', { id, status });
|
|
this.fetchTasks();
|
|
this.fetchJournal();
|
|
},
|
|
async deleteTask(id) {
|
|
if(!confirm("Wirklich löschen?")) return;
|
|
await axios.get('/WarehouseProject/deleteTask?id=' + id);
|
|
this.fetchTasks();
|
|
this.fetchJournal();
|
|
},
|
|
async addMember() {
|
|
if (!this.selectedUserId) return;
|
|
try {
|
|
await axios.post('/WarehouseProject/addTeamMember', { projectId: this.project.id, userId: this.selectedUserId });
|
|
this.fetchTeam();
|
|
this.fetchJournal();
|
|
this.selectedUserId = '';
|
|
} catch (e) {
|
|
alert("Fehler beim Hinzufügen (evtl. bereits im Team?)");
|
|
}
|
|
},
|
|
async removeMember(id) {
|
|
await axios.get('/WarehouseProject/removeTeamMember?id=' + id);
|
|
this.fetchTeam();
|
|
this.fetchJournal();
|
|
},
|
|
async openLinkOrderModal() {
|
|
await this.fetchAvailableOrders();
|
|
$(this.$refs.linkOrderModal).modal('show');
|
|
},
|
|
async linkOrder(orderId) {
|
|
if (!orderId) return;
|
|
try {
|
|
await axios.post('/WarehouseProject/linkOrder', { projectId: this.project.id, orderId });
|
|
$(this.$refs.linkOrderModal).modal('hide');
|
|
this.fetchLinkedOrders();
|
|
this.fetchJournal();
|
|
} catch (e) {
|
|
alert("Fehler beim Verknüpfen");
|
|
}
|
|
},
|
|
async unlinkOrder(id) {
|
|
await axios.get('/WarehouseProject/unlinkOrder?id=' + id);
|
|
this.fetchLinkedOrders();
|
|
this.fetchJournal();
|
|
},
|
|
async postJournal() {
|
|
if (!this.newJournalMessage.trim()) return;
|
|
await axios.post('/WarehouseProject/createJournalEntry', { projectId: this.project.id, message: this.newJournalMessage });
|
|
this.newJournalMessage = '';
|
|
this.fetchJournal();
|
|
}
|
|
}
|
|
});
|