Preorder/add new filter

This commit is contained in:
Luca Haid
2025-12-02 14:46:22 +00:00
parent 1985865d1f
commit 09ffd911fc
20 changed files with 1486 additions and 66 deletions
@@ -54,7 +54,7 @@ Vue.component('warehouse-offer-create-basic-offer-modal', {
totalDiscount: 0,
paymentTerms: 'net30',
deliveryTerms: 'ex_works',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n\nDiese Angebot hat eine Gültigkeit von 4 Wochen.\n\nVerrechnung erfolgt nach tatsächlichem Aufwand.\n\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n\nVerrechnung erfolgt nach tatsächlichem Aufwand.\n\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.',
notes: '',
},
@@ -147,8 +147,7 @@ Vue.component('warehouse-offer-modal', {
totalDiscount: 0,
paymentTerms: 'net30',
deliveryTerms: 'ex_works',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\nDiese Angebot hat eine Gültigkeit von 4 Wochen.\nVerrechnung erfolgt nach tatsächlichem Aufwand.\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.',
notes: '',
closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\nVerrechnung erfolgt nach tatsächlichem Aufwand.\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.', notes: '',
},
templateName: '',
ignoreFirstAddressChange: false,
@@ -1,11 +1,701 @@
Vue.component('warehouse-project', {
template: `
<tt-card>
<tt-table-crud ref="table">
</tt-table-crud>
<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 {window: window}
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();
}
}
});
@@ -0,0 +1,146 @@
Vue.component('warehouse-project-modal', {
props: {
id: { type: [String, Number], required: true },
},
data() {
return {
window: window,
loading: false,
allUsers: [],
project: {
title: '',
description: '',
startDate: null,
endDate: null,
storageLocation: '',
externalTeam: '',
internalTeam: [], // Array of user IDs
status: 'new',
financials: 0.00
},
statusOptions: [
{ text: 'Neu', value: 'new' },
{ text: 'In Bearbeitung', value: 'wip' },
{ text: 'Abgeschlossen', value: 'finished' },
{ text: 'Storniert', value: 'cancelled' }
]
};
},
//language=Vue
template: `
<tt-modal :show="true"
:delete="false"
:title="id === 'create' ? 'Projekt erstellen' : 'Projekt bearbeiten'"
@update:show="$emit('close')"
@submit="submit"
:save-loading="loading"
>
<div style="width: 99%">
<tt-input v-model="project.title" label="Bezeichnung *" sm row required/>
<div class="row">
<div class="col-md-6">
<tt-input v-model="project.startDate" label="Startdatum *" type="date" sm row required/>
</div>
<div class="col-md-6">
<tt-input v-model="project.endDate" label="Enddatum *" type="date" sm row required/>
</div>
</div>
<tt-input v-model="project.storageLocation" label="Lagerort" sm row/>
<div class="form-group row">
<label class="col-sm-3 col-form-label col-form-label-sm">Status *</label>
<div class="col-sm-9">
<select class="form-control form-control-sm" v-model="project.status" required>
<option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">{{ opt.text }}</option>
</select>
</div>
</div>
<!-- Internal Team Select (Create Only or simplified list) -->
<div class="form-group row" v-if="id === 'create'">
<label class="col-sm-3 col-form-label col-form-label-sm">Internes Team</label>
<div class="col-sm-9">
<select class="form-control form-control-sm" v-model="project.internalTeam" multiple style="height: 100px;">
<option v-for="u in allUsers" :key="u.id" :value="u.id">{{ u.name }}</option>
</select>
<small class="text-muted">Strg+Klick für Mehrfachauswahl</small>
</div>
</div>
<tt-textarea v-model="project.externalTeam" label="Externes Team" sm row placeholder="Namen, Firmen, Kontakte..."/>
<tt-textarea v-model="project.description" label="Beschreibung" sm row rows="4"/>
<div v-if="id !== 'create'" class="form-group row">
<label class="col-sm-3 col-form-label col-form-label-sm">Finanzen</label>
<div class="col-sm-9">
<input type="text" class="form-control form-control-sm" :value="formatPrice(project.financials)" disabled>
<small class="text-muted">Wird automatisch berechnet.</small>
</div>
</div>
</div>
</tt-modal>
`,
async mounted() {
this.fetchUsers();
if (this.id !== 'create') {
this.loading = true;
try {
const { data } = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } });
if (data) {
this.project = { ...this.project, ...data };
// Ensure dates are in YYYY-MM-DD format for input type="date"
if (this.project.startDate) this.project.startDate = this.formatDateForInput(this.project.startDate);
if (this.project.endDate) this.project.endDate = this.formatDateForInput(this.project.endDate);
}
} catch (e) {
console.error(e);
window.notify('error', 'Projekt konnte nicht geladen werden.');
} finally {
this.loading = false;
}
}
},
methods: {
async fetchUsers() {
try {
const res = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getUsers`);
this.allUsers = res.data;
} catch(e) { console.error(e); }
},
formatDateForInput(timestamp) {
if (!timestamp) return null;
return new Date(timestamp * 1000).toISOString().split('T')[0];
},
formatPrice(val) {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val || 0);
},
async submit() {
if (!this.project.title || !this.project.startDate || !this.project.endDate) {
return window.notify('error', 'Bitte füllen Sie alle Pflichtfelder aus (*).');
}
this.loading = true;
try {
const payload = { ...this.project };
if (payload.startDate) payload.startDate = Math.floor(new Date(payload.startDate).getTime() / 1000);
if (payload.endDate) payload.endDate = Math.floor(new Date(payload.endDate).getTime() / 1000);
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/${this.id === 'create' ? 'create' : 'update'}`;
const response = await axios.post(url, payload);
if (response.data.success) {
this.$emit('close');
window.notify('success', response.data.message || 'Gespeichert.');
} else {
window.notify('error', response.data.message || 'Fehler beim Speichern.');
}
} catch (e) {
console.error(e);
window.notify('error', 'Ein Systemfehler ist aufgetreten.');
} finally {
this.loading = false;
}
}
}
});