Xinon mobile/improve
This commit is contained in:
@@ -0,0 +1,761 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user