Preorder/add new filter
This commit is contained in:
@@ -2,46 +2,378 @@
|
||||
|
||||
class WarehouseProjectController extends TTCrud {
|
||||
protected string $headerTitle = 'Projekte';
|
||||
protected string $createText = 'Neues Projekt erstellen';
|
||||
protected string $singleText = 'Projekt';
|
||||
protected bool $createText = false;
|
||||
|
||||
//@formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'description', 'text' => 'Projektbeschreibung', 'modal' => ['type' => 'textarea']],
|
||||
|
||||
['key' => 'startDate', 'text' => 'Startdatum', 'required' => true, 'modal' => ['type' => 'datepicker']],
|
||||
['key' => 'endDate', 'text' => 'Enddatum', 'required' => true, 'modal' => ['type' => 'datepicker']],
|
||||
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => ['type' => 'positions-manager', 'config' => [
|
||||
'header' => 'Positionen',
|
||||
'fields' => [
|
||||
'articleId' => ['apiUrl' => '/WarehouseArticle/autoComplete','type' => 'autocomplete','customFieldReference' => 'WarehouseArticle','label' => 'Artikel'],
|
||||
'amount' => ['type' => 'input', 'label' => 'Menge', 'inputType' => 'number'],
|
||||
'purpose' => ['type' => 'input', 'label' => 'Zweck'],
|
||||
],
|
||||
'validateFormOptions' => [
|
||||
['key' => 'articleId', 'message' => 'Bitte füllen Sie den Artikel aus'],
|
||||
['key' => 'amount', 'message' => 'Bitte füllen Sie die Menge aus'],
|
||||
['key' => 'purpose', 'message' => 'Bitte füllen Sie den Zweck aus'],
|
||||
],
|
||||
]], 'table' => false],
|
||||
['key' => 'linkedOrderIds', 'text' => 'Verlinkte Bestellung', 'modal' => false],
|
||||
//
|
||||
['key' => 'assignedPersons', 'text' => 'Zugewiesene Personen', 'modal' => ['type' => 'positions-manager', 'config' => [
|
||||
'header' => 'Zugewiesene Personen',
|
||||
'fields' => [
|
||||
'userId' => ['apiUrl' => '/WarehouseShippingNote/userAutoComplete','type' => 'autocomplete','label' => 'Person','customFieldReference' => 'User']
|
||||
],
|
||||
'validateFormOptions' => [
|
||||
['key' => 'userId', 'message' => 'Bitte füllen Sie die Person aus'],
|
||||
],
|
||||
]], 'table' => false],
|
||||
|
||||
['key' => 'storageLocation', 'text' => 'Lagerort', 'modal' => ['type' => 'input']],
|
||||
['key' => 'note', 'text' => 'Notiz', 'modal' => ['type' => 'textarea']],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['visible' => false, 'type' => 'select'], 'table' => ['filter' => 'select']],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
|
||||
['key' => 'id', 'text' => 'ID', 'table' => false, 'modal' => false],
|
||||
['key' => 'projectNumber', 'text' => 'Projekt-Nr.', 'required' => false, 'modal' => ['disabled' => true, 'placeholder' => 'Wird automatisch generiert']],
|
||||
['key' => 'title', 'text' => 'Bezeichnung', 'required' => true],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
|
||||
['value' => 'new', 'text' => 'Neu'],
|
||||
['value' => 'wip', 'text' => 'In Bearbeitung'],
|
||||
['value' => 'finished', 'text' => 'Abgeschlossen'],
|
||||
['value' => 'cancelled', 'text' => 'Storniert'],
|
||||
]], 'table' => ['filter' => 'select']],
|
||||
['key' => 'startDate', 'text' => 'Startdatum', 'required' => true, 'modal' => ['type' => 'date'], 'table' => ['filter' => 'date']],
|
||||
['key' => 'endDate', 'text' => 'Enddatum', 'required' => true, 'modal' => ['type' => 'date'], 'table' => ['filter' => 'date']],
|
||||
['key' => 'financials', 'text' => 'Gesamtsumme', 'required' => false, 'modal' => ['disabled' => true], 'table' => ['formatter' => 'formatPrice']],
|
||||
['key' => 'storageLocation', 'text' => 'Lagerort', 'required' => false],
|
||||
['key' => 'externalTeam', 'text' => 'Externes Team', 'required' => false, 'modal' => ['type' => 'textarea'], 'table' => false],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => false, 'modal' => ['type' => 'textarea'], 'table' => false],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['formatter' => 'formatDate']],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
protected array $permissionCheck = ['WarehouseUser'];
|
||||
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => false];
|
||||
|
||||
protected function prepareCrudConfig(): void {
|
||||
if ($this->user->can('WarehouseAdmin')) {
|
||||
$this->additionalJSVariables['WAREHOUSE_ADMIN'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private array $tempInternalTeam = [];
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
$json = json_decode(file_get_contents('php://input'), true);
|
||||
if ($json) {
|
||||
$this->postData = array_merge($this->postData ?? [], $json);
|
||||
}
|
||||
|
||||
if (isset($this->postData['internalTeam'])) {
|
||||
$this->tempInternalTeam = $this->postData['internalTeam'];
|
||||
unset($this->postData['internalTeam']);
|
||||
}
|
||||
|
||||
$this->postData['projectNumber'] = WarehouseProjectModel::getNextProjectNumber();
|
||||
// Ensure defaults if not provided
|
||||
if (!isset($this->postData['status'])) $this->postData['status'] = 'new';
|
||||
if (!isset($this->postData['financials'])) $this->postData['financials'] = 0.00;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function afterCreate($id, $postData): void
|
||||
{
|
||||
WarehouseProjectJournalModel::create([
|
||||
'projectId' => $id,
|
||||
'text' => 'Projekt erstellt.',
|
||||
'createBy' => $this->user->id,
|
||||
'create' => time()
|
||||
]);
|
||||
|
||||
// Handle initial Internal Team
|
||||
if (!empty($this->tempInternalTeam) && is_array($this->tempInternalTeam)) {
|
||||
foreach ($this->tempInternalTeam as $userId) {
|
||||
WarehouseProjectMemberModel::create([
|
||||
'projectId' => $id,
|
||||
'userId' => $userId,
|
||||
'create' => time()
|
||||
]);
|
||||
|
||||
$u = UserModel::getOne($userId);
|
||||
$this->logJournal($id, "Teammitglied initial hinzugefügt: " . ($u ? $u->name : $userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterUpdate($id, $postData): void
|
||||
{
|
||||
// Simple journaling of main record update
|
||||
WarehouseProjectJournalModel::create([
|
||||
'projectId' => $id,
|
||||
'text' => 'Projektstammdaten aktualisiert.',
|
||||
'createBy' => $this->user->id,
|
||||
'create' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
// --- API for Vue ---
|
||||
|
||||
public function getProjectDetailsAction() {
|
||||
$id = $this->request->id;
|
||||
if (!$id) self::sendError("Projekt ID fehlt");
|
||||
|
||||
$project = WarehouseProjectModel::get($id);
|
||||
if (!$project) self::sendError("Projekt nicht gefunden");
|
||||
|
||||
self::returnJson(['project' => $project]);
|
||||
}
|
||||
|
||||
public function getTasksAction() {
|
||||
$projectId = $this->request->id;
|
||||
if (!$projectId) self::sendError("Projekt ID fehlt");
|
||||
|
||||
$tasks = WarehouseProjectTaskModel::getAll(['projectId' => $projectId], null, 0, ['key' => 'order', 'order' => 'ASC']);
|
||||
foreach ($tasks as $task) {
|
||||
if ($task->assignedUserId) {
|
||||
$user = UserModel::getOne($task->assignedUserId);
|
||||
$task->assignedUserName = $user ? $user->name : 'Unbekannt';
|
||||
} else {
|
||||
$task->assignedUserName = null;
|
||||
}
|
||||
}
|
||||
self::returnJson($tasks);
|
||||
}
|
||||
|
||||
public function saveTaskAction() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$projectId = $data['projectId'] ?? null;
|
||||
|
||||
if (!$projectId) self::sendError("Projekt ID fehlt");
|
||||
|
||||
$taskData = [
|
||||
'projectId' => $projectId,
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'] ?? '',
|
||||
'status' => $data['status'] ?? 'todo',
|
||||
'assignedUserId' => !empty($data['assignedUserId']) ? $data['assignedUserId'] : null,
|
||||
'createBy' => $this->user->id,
|
||||
'create' => time()
|
||||
];
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$existingTask = WarehouseProjectTaskModel::get($data['id']);
|
||||
if (!$existingTask) self::sendError("Aufgabe nicht gefunden");
|
||||
|
||||
// Merge existing data with new data to ensure all required fields are present
|
||||
$updatedData = array_merge((array)$existingTask, $taskData);
|
||||
$updatedData['id'] = $data['id']; // Ensure ID is in the data for update
|
||||
|
||||
// update method expects an array with 'id' key for update.
|
||||
WarehouseProjectTaskModel::update($updatedData);
|
||||
$this->logJournal($projectId, "Aufgabe aktualisiert: {$data['title']}");
|
||||
} else {
|
||||
// Get max order to append
|
||||
$count = WarehouseProjectTaskModel::count(['projectId' => $projectId]);
|
||||
$taskData['order'] = $count + 1;
|
||||
|
||||
WarehouseProjectTaskModel::create($taskData);
|
||||
$this->logJournal($projectId, "Aufgabe erstellt: {$data['title']}");
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function updateTaskStatusAction() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data['id']) || empty($data['status'])) self::sendError("Daten fehlen");
|
||||
|
||||
$task = WarehouseProjectTaskModel::get($data['id']);
|
||||
if ($task) {
|
||||
// Retrieve existing task data to preserve projectId and other required fields
|
||||
$updatedData = (array)$task;
|
||||
$updatedData['status'] = $data['status'];
|
||||
|
||||
// WarehouseProjectTaskModel::update expects an array with 'id' key for update.
|
||||
WarehouseProjectTaskModel::update($updatedData);
|
||||
$this->logJournal($task->projectId, "Aufgabenstatus '{$task->title}' geändert auf {$data['status']}");
|
||||
}
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteTaskAction() {
|
||||
$id = $this->request->id;
|
||||
if (!$id) self::sendError("ID fehlt");
|
||||
|
||||
$task = WarehouseProjectTaskModel::get($id);
|
||||
if ($task) {
|
||||
WarehouseProjectTaskModel::delete($id);
|
||||
$this->logJournal($task->projectId, "Aufgabe gelöscht: {$task->title}");
|
||||
}
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function getTeamAction() {
|
||||
$projectId = $this->request->id;
|
||||
if (!$projectId) self::sendError("ID fehlt");
|
||||
|
||||
$members = WarehouseProjectMemberModel::getAll(['projectId' => $projectId]);
|
||||
$users = [];
|
||||
foreach($members as $m) {
|
||||
$u = UserModel::getOne($m->userId);
|
||||
if ($u) {
|
||||
$users[] = [
|
||||
'memberId' => $m->id,
|
||||
'userId' => $u->id,
|
||||
'name' => $u->name,
|
||||
'role' => $m->role
|
||||
];
|
||||
}
|
||||
}
|
||||
self::returnJson($users);
|
||||
}
|
||||
|
||||
public function addTeamMemberAction() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data['projectId']) || empty($data['userId'])) self::sendError("Daten fehlen");
|
||||
|
||||
$exists = WarehouseProjectMemberModel::count(['projectId' => $data['projectId'], 'userId' => $data['userId']]);
|
||||
if ($exists > 0) self::sendError("Benutzer bereits im Team");
|
||||
|
||||
WarehouseProjectMemberModel::create([
|
||||
'projectId' => $data['projectId'],
|
||||
'userId' => $data['userId'],
|
||||
'role' => $data['role'] ?? null,
|
||||
'create' => time()
|
||||
]);
|
||||
|
||||
$u = UserModel::getOne($data['userId']);
|
||||
$this->logJournal($data['projectId'], "Teammitglied hinzugefügt: " . ($u ? $u->name : $data['userId']));
|
||||
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function removeTeamMemberAction() {
|
||||
$id = $this->request->id;
|
||||
$member = WarehouseProjectMemberModel::get($id);
|
||||
if ($member) {
|
||||
$u = UserModel::getOne($member->userId);
|
||||
WarehouseProjectMemberModel::delete($id);
|
||||
$this->logJournal($member->projectId, "Teammitglied entfernt: " . ($u ? $u->name : $member->userId));
|
||||
}
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function getAvailableOrderRequestsAction() {
|
||||
// Return open requests (not done, not cancelled)
|
||||
// You might want to filter out ones already linked to THIS project, but maybe not strictly necessary.
|
||||
$requests = WarehouseOrderRequest::getAll([], 100, 0, ['key' => 'create', 'order' => 'DESC']);
|
||||
|
||||
$available = [];
|
||||
foreach($requests as $r) {
|
||||
if (!$r->done && !$r->cancelled) {
|
||||
$available[] = [
|
||||
'id' => $r->id,
|
||||
'purpose' => $r->purpose,
|
||||
'create' => $r->create
|
||||
];
|
||||
}
|
||||
}
|
||||
self::returnJson($available);
|
||||
}
|
||||
|
||||
public function getLinkedOrdersAction() {
|
||||
$projectId = $this->request->id;
|
||||
if (!$projectId) self::sendError("ID fehlt");
|
||||
|
||||
$links = WarehouseProjectOrderRequestModel::getAll(['projectId' => $projectId]);
|
||||
$result = [];
|
||||
|
||||
foreach($links as $l) {
|
||||
$req = WarehouseOrderRequest::get($l->orderRequestId);
|
||||
if ($req) {
|
||||
$positions = json_decode($req->positions, true);
|
||||
|
||||
// Resolve actual Orders
|
||||
$orders = [];
|
||||
$ids = [];
|
||||
|
||||
if (!empty($req->linkedOrderIds)) {
|
||||
// Check if it is JSON array
|
||||
$decoded = json_decode($req->linkedOrderIds, true);
|
||||
if (is_array($decoded)) {
|
||||
$ids = $decoded;
|
||||
} else {
|
||||
// Fallback to comma separated
|
||||
$ids = explode(',', $req->linkedOrderIds);
|
||||
}
|
||||
}
|
||||
|
||||
foreach($ids as $oid) {
|
||||
$oid = trim($oid);
|
||||
if (empty($oid)) continue;
|
||||
|
||||
$o = WarehouseOrderModel::get($oid);
|
||||
if ($o) {
|
||||
$orders[] = [
|
||||
'id' => $o->id,
|
||||
'orderNumber' => $o->orderNumber,
|
||||
'status' => $o->status,
|
||||
'distributorId' => $o->distributorId
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'linkId' => $l->id,
|
||||
'requestId' => $req->id,
|
||||
'purpose' => $req->purpose,
|
||||
'create' => $req->create,
|
||||
'status' => $req->done ? 'done' : ($req->cancelled ? 'cancelled' : 'open'),
|
||||
'positionsCount' => is_array($positions) ? count($positions) : 0,
|
||||
'orders' => $orders
|
||||
];
|
||||
}
|
||||
}
|
||||
self::returnJson($result);
|
||||
}
|
||||
|
||||
public function linkOrderAction() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data['projectId']) || empty($data['orderId'])) self::sendError("Daten fehlen");
|
||||
|
||||
$order = WarehouseOrderRequest::get($data['orderId']);
|
||||
if (!$order) self::sendError("Bestellwunsch nicht gefunden");
|
||||
|
||||
WarehouseProjectOrderRequestModel::create([
|
||||
'projectId' => $data['projectId'],
|
||||
'orderRequestId' => $data['orderId'],
|
||||
'create' => time()
|
||||
]);
|
||||
|
||||
$this->logJournal($data['projectId'], "Bestellwunsch #{$data['orderId']} verknüpft.");
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function unlinkOrderAction() {
|
||||
$id = $this->request->id;
|
||||
$link = WarehouseProjectOrderRequestModel::get($id);
|
||||
if ($link) {
|
||||
WarehouseProjectOrderRequestModel::delete($id);
|
||||
$this->logJournal($link->projectId, "Bestellwunsch #{$link->orderRequestId} Verknüpfung aufgehoben.");
|
||||
}
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function createJournalEntryAction() {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($data['projectId'])) self::sendError("Projekt ID fehlt");
|
||||
|
||||
$this->logJournal($data['projectId'], $data['message'] ?? '');
|
||||
self::returnJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function getJournalAction() {
|
||||
$projectId = $this->request->id;
|
||||
$logs = WarehouseProjectJournalModel::getAll(['projectId' => $projectId], null, 0, ['order' => 'DESC', 'key' => 'create']);
|
||||
|
||||
foreach($logs as $log) {
|
||||
$u = UserModel::getOne($log->createBy);
|
||||
$log->userName = $u ? $u->name : 'System';
|
||||
}
|
||||
|
||||
self::returnJson($logs);
|
||||
}
|
||||
|
||||
private function logJournal($projectId, $text) {
|
||||
WarehouseProjectJournalModel::create([
|
||||
'projectId' => $projectId,
|
||||
'text' => $text,
|
||||
'createBy' => $this->user->id,
|
||||
'create' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
// Users for Team Selection
|
||||
public function getUsersAction() {
|
||||
$users = array_map(function($u) {
|
||||
return ['id' => $u->id, 'name' => $u->name];
|
||||
}, UserModel::search(['employee' => true]));
|
||||
self::returnJson($users);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,35 @@
|
||||
|
||||
class WarehouseProjectModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $projectNumber;
|
||||
public string $title;
|
||||
public string $description;
|
||||
public string $startDate;
|
||||
public string $endDate;
|
||||
public ?string $description;
|
||||
public ?int $startDate;
|
||||
public ?int $endDate;
|
||||
public string $status;
|
||||
public string $priority;
|
||||
|
||||
public int $assignedTo;
|
||||
public float $financials;
|
||||
public ?string $storageLocation;
|
||||
public ?string $externalTeam;
|
||||
public ?int $createdFromOrderId;
|
||||
public int $createBy;
|
||||
public int $create;
|
||||
|
||||
public static function getNextProjectNumber(): string {
|
||||
$year = date('Y');
|
||||
$prefix = "XP-$year-";
|
||||
|
||||
$db = self::getDB();
|
||||
$tableName = self::getFullyQualifiedTable();
|
||||
|
||||
$sql = "SELECT projectNumber FROM $tableName WHERE projectNumber LIKE '$prefix%' ORDER BY projectNumber DESC LIMIT 1";
|
||||
$result = $db->query($sql);
|
||||
|
||||
$nextNum = 1;
|
||||
if ($row = $result->fetch_assoc()) {
|
||||
$lastNumStr = substr($row['projectNumber'], strrpos($row['projectNumber'], '-') + 1);
|
||||
$nextNum = intval($lastNumStr) + 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad((string)$nextNum, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user