822 lines
28 KiB
PHP
822 lines
28 KiB
PHP
<?php
|
|
|
|
require_once APPDIR . 'MobileApp/Shared/MobileAppBaseHandler.php';
|
|
|
|
/**
|
|
* ShippingNote (Lieferschein) Handler
|
|
*
|
|
* Handles all endpoints for the Lager > ShippingNote module.
|
|
* API Base: /MobileApp/Lager/ShippingNote/{action}
|
|
*/
|
|
class ShippingNoteHandler extends MobileAppBaseHandler {
|
|
|
|
protected $requiredPermission = 'WarehouseUser';
|
|
|
|
// Office coordinates for distance calculation
|
|
const OFFICE_LAT = 46.99552810791587;
|
|
const OFFICE_LNG = 15.7751923956463;
|
|
|
|
public function initializeAction() {
|
|
$db = $this->db();
|
|
$userId = $this->user->id;
|
|
|
|
$userCar = null;
|
|
$sql = "SELECT id, number_plate, brand, model
|
|
FROM TimerecordingCar
|
|
WHERE user_id = {$userId}
|
|
AND (retired IS NULL OR retired = 0)
|
|
LIMIT 1";
|
|
$result = $db->query($sql);
|
|
if ($result && $row = $result->fetch_assoc()) {
|
|
$carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? ''));
|
|
if (!$carName) $carName = $row['number_plate'];
|
|
$userCar = [
|
|
'id' => intval($row['id']),
|
|
'name' => $carName,
|
|
'plate' => $row['number_plate'],
|
|
];
|
|
}
|
|
|
|
$allCars = [];
|
|
$sql = "SELECT id, number_plate, brand, model
|
|
FROM TimerecordingCar
|
|
WHERE (retired IS NULL OR retired = 0)
|
|
ORDER BY brand, model ASC";
|
|
$result = $db->query($sql);
|
|
while ($row = $result->fetch_assoc()) {
|
|
$carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? ''));
|
|
if (!$carName) $carName = $row['number_plate'];
|
|
$allCars[] = [
|
|
'id' => intval($row['id']),
|
|
'name' => $carName,
|
|
'plate' => $row['number_plate'],
|
|
];
|
|
}
|
|
|
|
$hourTypes = [
|
|
['id' => '', 'name' => 'Normal'],
|
|
['id' => '50', 'name' => '+50%'],
|
|
['id' => '100', 'name' => '+100%'],
|
|
['id' => 'regie', 'name' => 'Regie'],
|
|
];
|
|
|
|
$currentUser = [
|
|
'id' => $this->user->id,
|
|
'name' => $this->user->name,
|
|
'firstname' => $this->user->firstname ?? '',
|
|
'lastname' => $this->user->lastname ?? '',
|
|
];
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'userCar' => $userCar,
|
|
'allCars' => $allCars,
|
|
'hourTypes' => $hourTypes,
|
|
'currentUser' => $currentUser,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get customer by GPS location (nearest within radius)
|
|
* GET /MobileApp/Lager/ShippingNote/getCustomerByLocation?lat=X&lng=Y
|
|
*/
|
|
public function getCustomerByLocationAction() {
|
|
$lat = floatval($this->request->lat ?? 0);
|
|
$lng = floatval($this->request->lng ?? 0);
|
|
$radius = intval($this->request->radius ?? 200); // default 200 meters
|
|
|
|
if (!$lat || !$lng) {
|
|
self::returnJson(['success' => false, 'message' => 'Koordinaten fehlen']);
|
|
return;
|
|
}
|
|
|
|
$db = $this->db();
|
|
|
|
// Haversine formula for distance in meters
|
|
$sql = "SELECT id, customer_number, company, firstname, lastname, street, zip, city, email, phone, gps_lat, gps_long,
|
|
(6371000 * acos(
|
|
cos(radians({$lat})) * cos(radians(gps_lat)) *
|
|
cos(radians(gps_long) - radians({$lng})) +
|
|
sin(radians({$lat})) * sin(radians(gps_lat))
|
|
)) AS distance
|
|
FROM Address
|
|
WHERE gps_lat IS NOT NULL
|
|
AND gps_long IS NOT NULL
|
|
AND customer_number > 0
|
|
HAVING distance < {$radius}
|
|
ORDER BY distance ASC
|
|
LIMIT 1";
|
|
|
|
$result = $db->query($sql);
|
|
|
|
if ($result && $row = $result->fetch_assoc()) {
|
|
// Build display name
|
|
$displayName = $row['company'] ?: trim($row['firstname'] . ' ' . $row['lastname']);
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'found' => true,
|
|
'customer' => [
|
|
'id' => intval($row['id']),
|
|
'customerNumber' => $row['customer_number'],
|
|
'displayName' => $displayName,
|
|
'company' => $row['company'],
|
|
'firstname' => $row['firstname'],
|
|
'lastname' => $row['lastname'],
|
|
'street' => $row['street'],
|
|
'zip' => $row['zip'],
|
|
'city' => $row['city'],
|
|
'email' => $row['email'],
|
|
'phone' => $row['phone'],
|
|
'distance' => round(floatval($row['distance'])),
|
|
]
|
|
]);
|
|
} else {
|
|
self::returnJson([
|
|
'success' => true,
|
|
'found' => false,
|
|
'message' => 'Kein Kunde in der Nähe gefunden'
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reverse geocode coordinates to address
|
|
* GET /MobileApp/Lager/ShippingNote/reverseGeocode?lat=X&lng=Y
|
|
*/
|
|
public function reverseGeocodeAction() {
|
|
$lat = floatval($this->request->lat ?? 0);
|
|
$lng = floatval($this->request->lng ?? 0);
|
|
|
|
if (!$lat || !$lng) {
|
|
self::returnJson(['success' => false, 'message' => 'Koordinaten fehlen']);
|
|
return;
|
|
}
|
|
|
|
// Use Google Maps Geocoding API
|
|
$apiKey = defined('TT_GEOCODING_API_SECRET') ? TT_GEOCODING_API_SECRET : '';
|
|
if (!$apiKey) {
|
|
self::returnJson(['success' => false, 'message' => 'Google Maps API nicht konfiguriert']);
|
|
return;
|
|
}
|
|
|
|
$url = "https://maps.googleapis.com/maps/api/geocode/json?latlng={$lat},{$lng}&key={$apiKey}&language=de";
|
|
|
|
$response = @file_get_contents($url);
|
|
if (!$response) {
|
|
self::returnJson(['success' => false, 'message' => 'Geocoding fehlgeschlagen']);
|
|
return;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
if ($data['status'] !== 'OK' || empty($data['results'])) {
|
|
self::returnJson(['success' => false, 'message' => 'Keine Adresse gefunden']);
|
|
return;
|
|
}
|
|
|
|
// Parse address components
|
|
$result = $data['results'][0];
|
|
$components = $result['address_components'];
|
|
|
|
$street = '';
|
|
$streetNumber = '';
|
|
$zip = '';
|
|
$city = '';
|
|
|
|
foreach ($components as $comp) {
|
|
if (in_array('route', $comp['types'])) {
|
|
$street = $comp['long_name'];
|
|
}
|
|
if (in_array('street_number', $comp['types'])) {
|
|
$streetNumber = $comp['long_name'];
|
|
}
|
|
if (in_array('postal_code', $comp['types'])) {
|
|
$zip = $comp['long_name'];
|
|
}
|
|
if (in_array('locality', $comp['types'])) {
|
|
$city = $comp['long_name'];
|
|
}
|
|
}
|
|
|
|
$fullStreet = trim($street . ' ' . $streetNumber);
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'address' => [
|
|
'street' => $fullStreet,
|
|
'zip' => $zip,
|
|
'city' => $city,
|
|
'formatted' => $result['formatted_address'] ?? '',
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Search customers by name/company
|
|
* GET /MobileApp/Lager/ShippingNote/searchCustomers?query=X
|
|
*/
|
|
public function searchCustomersAction() {
|
|
$query = trim($this->request->query ?? '');
|
|
|
|
if (strlen($query) < 1) {
|
|
self::returnJson(['success' => true, 'customers' => []]);
|
|
return;
|
|
}
|
|
|
|
$db = $this->db();
|
|
|
|
// Split query into words for lazy search (e.g., "fab her" matches "Fabian Herbst")
|
|
$words = preg_split('/\s+/', trim($query));
|
|
$wordConditions = [];
|
|
|
|
foreach ($words as $word) {
|
|
if (strlen($word) < 1) continue;
|
|
$escapedWord = $db->escape($word);
|
|
$wordConditions[] = "(company LIKE '%{$escapedWord}%'
|
|
OR firstname LIKE '%{$escapedWord}%'
|
|
OR lastname LIKE '%{$escapedWord}%'
|
|
OR customer_number LIKE '%{$escapedWord}%')";
|
|
}
|
|
|
|
if (empty($wordConditions)) {
|
|
self::returnJson(['success' => true, 'customers' => []]);
|
|
return;
|
|
}
|
|
|
|
$whereClause = implode(' AND ', $wordConditions);
|
|
|
|
$sql = "SELECT id, customer_number, company, firstname, lastname, street, zip, city, email, phone, gps_lat, gps_long
|
|
FROM Address
|
|
WHERE customer_number > 0
|
|
AND ({$whereClause})
|
|
ORDER BY company, lastname, firstname
|
|
LIMIT 20";
|
|
|
|
$result = $db->query($sql);
|
|
$customers = [];
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
$displayName = $row['company'] ?: trim($row['firstname'] . ' ' . $row['lastname']);
|
|
$customers[] = [
|
|
'id' => intval($row['id']),
|
|
'customerNumber' => $row['customer_number'],
|
|
'displayName' => $displayName,
|
|
'company' => $row['company'],
|
|
'firstname' => $row['firstname'],
|
|
'lastname' => $row['lastname'],
|
|
'street' => $row['street'],
|
|
'zip' => $row['zip'],
|
|
'city' => $row['city'],
|
|
'email' => $row['email'],
|
|
'phone' => $row['phone'],
|
|
'gpsLat' => $row['gps_lat'] ? floatval($row['gps_lat']) : null,
|
|
'gpsLong' => $row['gps_long'] ? floatval($row['gps_long']) : null,
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'customers' => $customers]);
|
|
}
|
|
|
|
/**
|
|
* Search articles
|
|
* GET /MobileApp/Lager/ShippingNote/searchArticles?query=X
|
|
*/
|
|
public function searchArticlesAction() {
|
|
$query = trim($this->request->query ?? '');
|
|
|
|
if (strlen($query) < 1) {
|
|
self::returnJson(['success' => true, 'articles' => []]);
|
|
return;
|
|
}
|
|
|
|
$db = $this->db();
|
|
$escapedQuery = $db->escape($query);
|
|
|
|
$sql = "SELECT id, articleNumber, title, unit
|
|
FROM WarehouseArticle
|
|
WHERE (isEndOfLife IS NULL OR isEndOfLife = 0)
|
|
AND (articleNumber LIKE '%{$escapedQuery}%'
|
|
OR title LIKE '%{$escapedQuery}%')
|
|
ORDER BY title ASC
|
|
LIMIT 30";
|
|
|
|
$result = $db->query($sql);
|
|
$articles = [];
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
$articles[] = [
|
|
'id' => intval($row['id']),
|
|
'articleNumber' => $row['articleNumber'],
|
|
'title' => $row['title'],
|
|
'unit' => $row['unit'] ?? 'Stk.',
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'articles' => $articles]);
|
|
}
|
|
|
|
/**
|
|
* Get article by QR code or article number
|
|
* GET /MobileApp/Lager/ShippingNote/getArticle?code=X
|
|
*/
|
|
public function getArticleAction() {
|
|
$code = $this->request->code ?? '';
|
|
|
|
if (!$code) {
|
|
self::returnJson(['success' => false, 'message' => 'Kein Code angegeben']);
|
|
return;
|
|
}
|
|
|
|
$articleId = null;
|
|
|
|
// Check for QR code format WA:ID: or WH:ID:
|
|
if (preg_match('/^(?:WA|WH):(\d+):/', $code, $matches)) {
|
|
$articleId = intval($matches[1]);
|
|
} else {
|
|
// Try to find by article number
|
|
$article = WarehouseArticleModel::getFirst(['articleNumber' => $code]);
|
|
if ($article) {
|
|
$articleId = $article->id;
|
|
}
|
|
}
|
|
|
|
if (!$articleId) {
|
|
self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
$article = WarehouseArticleModel::get($articleId);
|
|
if (!$article) {
|
|
self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'article' => [
|
|
'id' => $article->id,
|
|
'articleNumber' => $article->articleNumber,
|
|
'title' => $article->title,
|
|
'unit' => $article->unit ?? 'Stk.',
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get user's assigned car
|
|
* GET /MobileApp/Lager/ShippingNote/getUserCar?userId=X
|
|
*/
|
|
public function getUserCarAction() {
|
|
$userId = intval($this->request->userId ?? $this->user->id);
|
|
|
|
$db = $this->db();
|
|
|
|
// Get user's assigned car from TimerecordingCar (user_id is on TimerecordingCar)
|
|
$sql = "SELECT id, number_plate, brand, model
|
|
FROM TimerecordingCar
|
|
WHERE user_id = {$userId}
|
|
AND (retired IS NULL OR retired = 0)
|
|
LIMIT 1";
|
|
|
|
$result = $db->query($sql);
|
|
|
|
if ($result && $row = $result->fetch_assoc()) {
|
|
$carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? ''));
|
|
if (!$carName) $carName = $row['number_plate'];
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'car' => [
|
|
'id' => intval($row['id']),
|
|
'name' => $carName,
|
|
'plate' => $row['number_plate'],
|
|
]
|
|
]);
|
|
} else {
|
|
self::returnJson([
|
|
'success' => true,
|
|
'car' => null
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all available cars for selection
|
|
* GET /MobileApp/Lager/ShippingNote/getAllCars
|
|
*/
|
|
public function getAllCarsAction() {
|
|
$db = $this->db();
|
|
|
|
$sql = "SELECT id, number_plate, brand, model
|
|
FROM TimerecordingCar
|
|
WHERE (retired IS NULL OR retired = 0)
|
|
ORDER BY brand, model ASC";
|
|
|
|
$result = $db->query($sql);
|
|
$cars = [];
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
$carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? ''));
|
|
if (!$carName) $carName = $row['number_plate'];
|
|
|
|
$cars[] = [
|
|
'id' => intval($row['id']),
|
|
'name' => $carName,
|
|
'plate' => $row['number_plate'],
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'cars' => $cars]);
|
|
}
|
|
|
|
/**
|
|
* Get hour types for selection
|
|
* GET /MobileApp/Lager/ShippingNote/getHourTypes
|
|
*/
|
|
public function getHourTypesAction() {
|
|
// Hour types matching desktop modal
|
|
$hourTypes = [
|
|
['id' => '', 'name' => 'Normal'],
|
|
['id' => '50', 'name' => '+50%'],
|
|
['id' => '100', 'name' => '+100%'],
|
|
['id' => 'regie', 'name' => 'Regie'],
|
|
];
|
|
|
|
self::returnJson(['success' => true, 'hourTypes' => $hourTypes]);
|
|
}
|
|
|
|
/**
|
|
* Calculate round-trip distance from office to coordinates
|
|
* GET /MobileApp/Lager/ShippingNote/calculateDistance?lat=X&lng=Y
|
|
*/
|
|
public function calculateDistanceAction() {
|
|
$lat = floatval($this->request->lat ?? 0);
|
|
$lng = floatval($this->request->lng ?? 0);
|
|
|
|
if (!$lat || !$lng) {
|
|
self::returnJson(['success' => false, 'message' => 'Koordinaten fehlen']);
|
|
return;
|
|
}
|
|
|
|
// Use estimation on localhost for development
|
|
$isLocalhost = in_array($_SERVER['HTTP_HOST'] ?? '', ['localhost', '127.0.0.1']);
|
|
|
|
// Use Google Distance Matrix API for accurate driving distance
|
|
$apiKey = defined('TT_GEOCODING_API_SECRET') ? TT_GEOCODING_API_SECRET : '';
|
|
if (!$apiKey || $isLocalhost) {
|
|
// Fallback to straight-line distance * 1.3 (rough road factor)
|
|
$distance = $this->haversineDistance(self::OFFICE_LAT, self::OFFICE_LNG, $lat, $lng);
|
|
$kmOneWay = round($distance / 1000 * 1.3, 1);
|
|
$kmRoundTrip = $kmOneWay * 2;
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'distanceOneWay' => $kmOneWay,
|
|
'distanceRoundTrip' => $kmRoundTrip,
|
|
'estimated' => true
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$origin = self::OFFICE_LAT . ',' . self::OFFICE_LNG;
|
|
$destination = "{$lat},{$lng}";
|
|
$url = "https://maps.googleapis.com/maps/api/distancematrix/json?origins={$origin}&destinations={$destination}&mode=driving&key={$apiKey}";
|
|
|
|
$response = @file_get_contents($url);
|
|
if (!$response) {
|
|
self::returnJson(['success' => false, 'message' => 'Distanzberechnung fehlgeschlagen']);
|
|
return;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
if ($data['status'] !== 'OK' || empty($data['rows'][0]['elements'][0]['distance'])) {
|
|
self::returnJson(['success' => false, 'message' => 'Keine Route gefunden']);
|
|
return;
|
|
}
|
|
|
|
$distanceMeters = $data['rows'][0]['elements'][0]['distance']['value'];
|
|
$kmOneWay = round($distanceMeters / 1000, 1);
|
|
$kmRoundTrip = $kmOneWay * 2;
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'distanceOneWay' => $kmOneWay,
|
|
'distanceRoundTrip' => $kmRoundTrip,
|
|
'estimated' => false
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create new shipping note
|
|
* POST /MobileApp/Lager/ShippingNote/create
|
|
*/
|
|
public function createAction() {
|
|
$postData = $this->getPostData();
|
|
|
|
// Validate required fields
|
|
$requiredFields = ['deliveryAddressName', 'deliveryAddressLine', 'deliveryAddressPLZ', 'deliveryAddressCity', 'note'];
|
|
foreach ($requiredFields as $field) {
|
|
if (empty($postData[$field])) {
|
|
self::returnJson(['success' => false, 'message' => "Feld '{$field}' ist erforderlich"]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Must have at least positions OR hoursEntries
|
|
$positions = $postData['positions'] ?? [];
|
|
$hoursEntries = $postData['hoursEntries'] ?? [];
|
|
|
|
if (empty($positions) && empty($hoursEntries)) {
|
|
self::returnJson(['success' => false, 'message' => 'Mindestens eine Position oder Stundenbuchung erforderlich']);
|
|
return;
|
|
}
|
|
|
|
$db = $this->db();
|
|
|
|
// Prepare data
|
|
$data = [
|
|
'status' => 'new',
|
|
'type' => null,
|
|
'billingAddressId' => null,
|
|
'deliveryAddressName' => $db->escape($postData['deliveryAddressName']),
|
|
'deliveryAddressLine' => $db->escape($postData['deliveryAddressLine']),
|
|
'deliveryAddressPLZ' => $db->escape($postData['deliveryAddressPLZ']),
|
|
'deliveryAddressCity' => $db->escape($postData['deliveryAddressCity']),
|
|
'deliveryAddressEMail' => $db->escape($postData['deliveryAddressEMail'] ?? ''),
|
|
'note' => $db->escape($postData['note']),
|
|
'positions' => json_encode($positions),
|
|
'hoursEntries' => json_encode($hoursEntries),
|
|
'textElements' => json_encode($postData['textElements'] ?? []),
|
|
'metadata' => json_encode($postData['metadata'] ?? []),
|
|
'create' => time(),
|
|
'createBy' => $this->user->id,
|
|
];
|
|
|
|
// Generate shipping note number
|
|
$shippingNoteNumber = WarehouseShippingNoteModel::generateShippingNoteNumber();
|
|
$data['shippingNoteNumber'] = $shippingNoteNumber;
|
|
|
|
// Build INSERT query
|
|
$columns = implode(', ', array_map(function($k) { return "`{$k}`"; }, array_keys($data)));
|
|
$values = implode(', ', array_map(function($v) {
|
|
return $v === null ? 'NULL' : "'{$v}'";
|
|
}, array_values($data)));
|
|
|
|
$db->query("INSERT INTO WarehouseShippingNote ({$columns}) VALUES ({$values})");
|
|
$id = $db->insert_id;
|
|
|
|
if (!$id) {
|
|
self::returnJson(['success' => false, 'message' => 'Fehler beim Speichern']);
|
|
return;
|
|
}
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'message' => 'Lieferschein erstellt',
|
|
'shippingNote' => [
|
|
'id' => $id,
|
|
'shippingNoteNumber' => $shippingNoteNumber,
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sign a shipping note
|
|
* POST /MobileApp/Lager/ShippingNote/sign?id=X
|
|
*/
|
|
public function signAction() {
|
|
$id = intval($this->request->id ?? 0);
|
|
$postData = $this->getPostData();
|
|
|
|
if (!$id) {
|
|
self::returnJson(['success' => false, 'message' => 'Lieferschein-ID fehlt']);
|
|
return;
|
|
}
|
|
|
|
$shippingNote = WarehouseShippingNoteModel::get($id);
|
|
if (!$shippingNote) {
|
|
self::returnJson(['success' => false, 'message' => 'Lieferschein nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
// Check if already signed
|
|
if (!empty($shippingNote->signature) || !empty($shippingNote->signatureName)) {
|
|
self::returnJson(['success' => false, 'message' => 'Bereits unterschrieben']);
|
|
return;
|
|
}
|
|
|
|
// Validate signature data
|
|
$signature = $postData['signature'] ?? '';
|
|
$signatureName = $postData['signatureName'] ?? '';
|
|
|
|
if (empty($signature) || empty($signatureName)) {
|
|
self::returnJson(['success' => false, 'message' => 'Unterschrift und Name erforderlich']);
|
|
return;
|
|
}
|
|
|
|
$db = $this->db();
|
|
$signatureEscaped = $db->escape($signature);
|
|
$signatureNameEscaped = $db->escape($signatureName);
|
|
$signatureDate = date('Y-m-d');
|
|
|
|
$db->query("UPDATE WarehouseShippingNote
|
|
SET signature = '{$signatureEscaped}',
|
|
signatureName = '{$signatureNameEscaped}',
|
|
signatureDate = '{$signatureDate}'
|
|
WHERE id = {$id}");
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'message' => 'Unterschrift gespeichert'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get my unsigned shipping notes
|
|
* GET /MobileApp/Lager/ShippingNote/getMyShippingNotes
|
|
*/
|
|
public function getMyShippingNotesAction() {
|
|
$onlyUnsigned = ($this->request->unsigned ?? '1') === '1';
|
|
$limit = intval($this->request->limit ?? 20);
|
|
|
|
$db = $this->db();
|
|
|
|
$whereClause = "createBy = {$this->user->id}";
|
|
if ($onlyUnsigned) {
|
|
$whereClause .= " AND (signature IS NULL OR signature = '')";
|
|
}
|
|
|
|
$sql = "SELECT id, shippingNoteNumber, status, type, deliveryAddressName, deliveryAddressLine,
|
|
deliveryAddressPLZ, deliveryAddressCity, note,
|
|
signature, signatureName, signatureDate, `create`
|
|
FROM WarehouseShippingNote
|
|
WHERE {$whereClause}
|
|
ORDER BY `create` DESC
|
|
LIMIT {$limit}";
|
|
|
|
$result = $db->query($sql);
|
|
$shippingNotes = [];
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
$shippingNotes[] = [
|
|
'id' => intval($row['id']),
|
|
'shippingNoteNumber' => $row['shippingNoteNumber'],
|
|
'status' => $row['status'],
|
|
'type' => $row['type'],
|
|
'deliveryAddressName' => $row['deliveryAddressName'],
|
|
'deliveryAddressLine' => $row['deliveryAddressLine'],
|
|
'deliveryAddressPLZ' => $row['deliveryAddressPLZ'],
|
|
'deliveryAddressCity' => $row['deliveryAddressCity'],
|
|
'note' => $row['note'],
|
|
'isSigned' => !empty($row['signature']),
|
|
'signatureName' => $row['signatureName'],
|
|
'signatureDate' => $row['signatureDate'],
|
|
'create' => date('d.m.Y H:i', $row['create']),
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'shippingNotes' => $shippingNotes]);
|
|
}
|
|
|
|
/**
|
|
* Get a single shipping note by ID
|
|
* GET /MobileApp/Lager/ShippingNote/getShippingNote?id=X
|
|
*/
|
|
public function getShippingNoteAction() {
|
|
$id = intval($this->request->id ?? 0);
|
|
|
|
if (!$id) {
|
|
self::returnJson(['success' => false, 'message' => 'ID fehlt']);
|
|
return;
|
|
}
|
|
|
|
$shippingNote = WarehouseShippingNoteModel::get($id);
|
|
if (!$shippingNote) {
|
|
self::returnJson(['success' => false, 'message' => 'Lieferschein nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
self::returnJson([
|
|
'success' => true,
|
|
'shippingNote' => [
|
|
'id' => intval($shippingNote->id),
|
|
'shippingNoteNumber' => $shippingNote->shippingNoteNumber,
|
|
'status' => $shippingNote->status,
|
|
'type' => $shippingNote->type,
|
|
'billingAddressId' => $shippingNote->billingAddressId,
|
|
'deliveryAddressName' => $shippingNote->deliveryAddressName,
|
|
'deliveryAddressLine' => $shippingNote->deliveryAddressLine,
|
|
'deliveryAddressPLZ' => $shippingNote->deliveryAddressPLZ,
|
|
'deliveryAddressCity' => $shippingNote->deliveryAddressCity,
|
|
'deliveryAddressEMail' => $shippingNote->deliveryAddressEMail,
|
|
'note' => $shippingNote->note,
|
|
'positions' => json_decode($shippingNote->positions, true) ?? [],
|
|
'hoursEntries' => json_decode($shippingNote->hoursEntries, true) ?? [],
|
|
'isSigned' => !empty($shippingNote->signature),
|
|
'signatureName' => $shippingNote->signatureName,
|
|
'signatureDate' => $shippingNote->signatureDate,
|
|
'create' => date('d.m.Y H:i', $shippingNote->create),
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get shipping note types
|
|
* GET /MobileApp/Lager/ShippingNote/getTypes
|
|
*/
|
|
public function getTypesAction() {
|
|
$types = [
|
|
['value' => 'V', 'text' => 'Verrechnen'],
|
|
['value' => 'XI', 'text' => 'Xinon Intern'],
|
|
['value' => 'XH', 'text' => 'Xinon Hersteller'],
|
|
['value' => 'SNOPP', 'text' => 'SNOPP'],
|
|
['value' => 'ESTMK', 'text' => 'Energie Steiermark'],
|
|
['value' => 'SBIDI', 'text' => 'SBIDI'],
|
|
];
|
|
|
|
self::returnJson(['success' => true, 'types' => $types]);
|
|
}
|
|
|
|
/**
|
|
* Get current user info (for pre-filling forms)
|
|
* GET /MobileApp/Lager/ShippingNote/getCurrentUser
|
|
*/
|
|
public function getCurrentUserAction() {
|
|
self::returnJson([
|
|
'success' => true,
|
|
'user' => [
|
|
'id' => $this->user->id,
|
|
'name' => $this->user->name,
|
|
'firstname' => $this->user->firstname ?? '',
|
|
'lastname' => $this->user->lastname ?? '',
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Search employees for multi-employee selection
|
|
* GET /MobileApp/Lager/ShippingNote/searchEmployees?query=X
|
|
*/
|
|
public function searchEmployeesAction() {
|
|
$query = trim($this->request->query ?? '');
|
|
|
|
$db = $this->db();
|
|
|
|
// Base query: active workers who have TimerecordingEmployee entry (= employees)
|
|
$sql = "SELECT w.id, w.name, w.email
|
|
FROM Worker w
|
|
INNER JOIN TimerecordingEmployee te ON te.user_id = w.id
|
|
WHERE w.active = 1";
|
|
|
|
// Add search filter if query provided
|
|
if (strlen($query) >= 1) {
|
|
// Split query into words for lazy search (e.g., "fab her" matches "Fabian Herbst")
|
|
$words = preg_split('/\s+/', trim($query));
|
|
$wordConditions = [];
|
|
|
|
foreach ($words as $word) {
|
|
if (strlen($word) < 1) continue;
|
|
$escapedWord = $db->escape($word);
|
|
$wordConditions[] = "(w.name LIKE '%{$escapedWord}%' OR w.email LIKE '%{$escapedWord}%')";
|
|
}
|
|
|
|
if (!empty($wordConditions)) {
|
|
$sql .= " AND " . implode(' AND ', $wordConditions);
|
|
}
|
|
}
|
|
|
|
$sql .= " ORDER BY w.name ASC LIMIT 20";
|
|
|
|
$result = $db->query($sql);
|
|
$employees = [];
|
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
$employees[] = [
|
|
'id' => intval($row['id']),
|
|
'name' => $row['name'],
|
|
'email' => $row['email'],
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'employees' => $employees]);
|
|
}
|
|
|
|
/**
|
|
* Helper: Calculate Haversine distance in meters
|
|
*/
|
|
private function haversineDistance($lat1, $lng1, $lat2, $lng2) {
|
|
$earthRadius = 6371000; // meters
|
|
|
|
$dLat = deg2rad($lat2 - $lat1);
|
|
$dLng = deg2rad($lng2 - $lng1);
|
|
|
|
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
|
sin($dLng / 2) * sin($dLng / 2);
|
|
|
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
|
|
return $earthRadius * $c;
|
|
}
|
|
}
|