Files
thetool/public/js/pages/WarehouseProject/WarehouseProject.js
2025-12-02 14:46:22 +00:00

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">&times;</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">&times;</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();
}
}
});