Compare commits
10
Commits
315f85356c
...
7010e15f16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7010e15f16 | ||
|
|
6312f6657b | ||
|
|
7e12c6ff1d | ||
|
|
6593e921ee | ||
|
|
a53ff3912c | ||
|
|
daf1ce3401 | ||
|
|
ee3f4694c2 | ||
|
|
be9e6ae4e5 | ||
|
|
ad8c6eaeca | ||
|
|
b4f3c4260a |
+66
-26
@@ -117,8 +117,47 @@ if (isset($_GET['returnto']) && $_GET['returnto'] == "pop-detail") {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-2 col-form-label">Adresssuche</label>
|
||||
<div class="col-lg-8">
|
||||
<?php
|
||||
$addrDisplay = '';
|
||||
if ($pop->street) {
|
||||
$addrDisplay = $pop->street;
|
||||
if ($pop->number) $addrDisplay .= ' ' . $pop->number;
|
||||
if ($pop->zip || $pop->city) $addrDisplay .= ', ' . trim($pop->zip . ' ' . $pop->city);
|
||||
}
|
||||
?>
|
||||
<select class="form-control" id="address_search">
|
||||
<?php if($addrDisplay): ?>
|
||||
<option value="preset" selected><?= htmlspecialchars($addrDisplay) ?></option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-2 col-form-label" for="zip">Adresse</label>
|
||||
<div class="col-lg-4">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Straße</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="street" id="street"
|
||||
value="<?= $pop->street ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Nr.</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="number" id="number"
|
||||
value="<?= $pop->number ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-2 col-form-label" for="zip"></label>
|
||||
<div class="col-lg-1">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
@@ -139,32 +178,7 @@ if (isset($_GET['returnto']) && $_GET['returnto'] == "pop-detail") {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<!-- <label class="col-lg-2 col-form-label" for="street">Straße</label>-->
|
||||
<!-- <div class="col-lg-3">-->
|
||||
<!-- <input type="text" class="form-control" name="street" id="street"-->
|
||||
<!-- value="--><?php //= $pop->street ?><!--">-->
|
||||
<!-- </div>-->
|
||||
<label class="col-lg-2 col-form-label" for=""></label>
|
||||
<div class="col-lg-4">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Straße</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="street" id="street"
|
||||
value="<?= $pop->street ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Nr.</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="number" id="number"
|
||||
value="<?= $pop->number ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,6 +294,32 @@ if (isset($_GET['returnto']) && $_GET['returnto'] == "pop-detail") {
|
||||
});
|
||||
|
||||
|
||||
// combined address search with select2
|
||||
$('#address_search').select2({
|
||||
placeholder: 'Straße Hausnummer, PLZ Ort eingeben...',
|
||||
allowClear: true,
|
||||
minimumInputLength: 2,
|
||||
ajax: {
|
||||
url: "<?=self::getUrl('Pop','api')?>",
|
||||
dataType: 'json',
|
||||
delay: 300,
|
||||
data: function(params) { return { do: 'searchAddress', q: params.term }; },
|
||||
processResults: function(data) { return { results: data.results || [] }; }
|
||||
}
|
||||
});
|
||||
|
||||
$('#address_search').on('select2:select', function(e) {
|
||||
var d = e.params.data;
|
||||
$('#street').val(d.street || '');
|
||||
$('#number').val(d.number || '');
|
||||
$('#zip').val(d.zip || '');
|
||||
$('#city').val(d.city || '');
|
||||
});
|
||||
|
||||
$('#address_search').on('select2:clear', function() {
|
||||
$('#street, #number, #zip, #city').val('');
|
||||
});
|
||||
|
||||
// network_id // owner_id
|
||||
// if get parameter network_id is set, select it if found
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -973,7 +973,7 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
}
|
||||
}
|
||||
|
||||
$requiredFlagIds = [3, 4];
|
||||
$requiredFlagIds = [3, 4, 5];
|
||||
$allFlagsChecked = true;
|
||||
|
||||
foreach ($requiredFlagIds as $flagId) {
|
||||
@@ -2505,7 +2505,11 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
fcpSelect.empty().select2({ data: [], placeholder: "Bitte Kampagne auswählen", allowClear: true });
|
||||
return;
|
||||
}
|
||||
$.get(apiUrl, { do: "getFCPsForCampaign", campaign_id: campaign_id }, (success) => {
|
||||
const filterData = $(this).closest("form").serializeArray().filter(item => item.name.startsWith("filter[") && !item.name.includes("fcp"));
|
||||
const params = { do: "getFCPsForCampaign", campaign_id: campaign_id };
|
||||
filterData.forEach(item => { params[item.name] = item.value; });
|
||||
|
||||
$.get(apiUrl, params, (success) => {
|
||||
let fcpData = [];
|
||||
let opts = { data: [], placeholder: "Bitte Kampagne auswählen", allowClear: true };
|
||||
if (success?.status === "OK" && Array.isArray(success.result)) {
|
||||
@@ -2520,11 +2524,9 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
fcpData.sort((a, b) => {
|
||||
if (a.id === "") return -1;
|
||||
if (b.id === "") return 1;
|
||||
// Sort by preorder_count descending
|
||||
if ((b.preorder_count || 0) !== (a.preorder_count || 0)) {
|
||||
return (b.preorder_count || 0) - (a.preorder_count || 0);
|
||||
}
|
||||
// Fallback to name-based numeric/alpha sort
|
||||
const aN = a.id.replace(/\D/g, ""), bN = b.id.replace(/\D/g, "");
|
||||
return aN && bN ? parseInt(aN, 10) - parseInt(bN, 10) : a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ class PopController extends mfBaseController
|
||||
"city" => $pop->city,
|
||||
"street" => $pop->street,
|
||||
"number" => $pop->number,
|
||||
"address" => trim($pop->street . ' ' . $pop->number . ', ' . $pop->zip . ' ' . $pop->city, ', '),
|
||||
"state" => $pop->state,
|
||||
"folder_link" => $pop->folder_link,
|
||||
"doku_date" => $pop->doku_date,
|
||||
@@ -1411,6 +1412,8 @@ class PopController extends mfBaseController
|
||||
case "getSplicePlanForElement":
|
||||
return $this->getSplicePlanForElementAction();
|
||||
break;
|
||||
case "searchAddress":
|
||||
return $this->searchAddressApi();
|
||||
default:
|
||||
$return = false;
|
||||
}
|
||||
@@ -1965,4 +1968,97 @@ class PopController extends mfBaseController
|
||||
'connections' => $result
|
||||
]));
|
||||
}
|
||||
|
||||
private function searchAddressApi()
|
||||
{
|
||||
$q = trim($this->request->q);
|
||||
if (strlen($q) < 2) {
|
||||
$this->returnJson(["results" => []]);
|
||||
}
|
||||
|
||||
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||
|
||||
// split input into parts, any order allowed: "graz alte 5" or "Alte Poststraße 5, 8010 Graz"
|
||||
$parts = preg_split('/[\s,]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// categorize: 4-digit = PLZ, short number = hausnummer, text = street or city
|
||||
$text_parts = [];
|
||||
$hausnummer = null;
|
||||
$plz = null;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (preg_match('/^\d{4}$/', $part)) {
|
||||
$plz = $part;
|
||||
} elseif ($hausnummer === null && preg_match('/^\d+[a-zA-Z]?$/', $part)) {
|
||||
$hausnummer = $part;
|
||||
} else {
|
||||
$text_parts[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-query Gemeinde (tiny table, instant) to resolve text→gemeinde_id
|
||||
$gemeinde_ids_per_part = [];
|
||||
foreach ($text_parts as $tp) {
|
||||
$escaped = $db->escape($tp);
|
||||
$gres = $db->query("SELECT id FROM Gemeinde WHERE name LIKE '%$escaped%'");
|
||||
$ids = [];
|
||||
if ($gres && $db->num_rows($gres)) {
|
||||
while ($grow = $db->fetch_object($gres)) {
|
||||
$ids[] = (int)$grow->id;
|
||||
}
|
||||
}
|
||||
$gemeinde_ids_per_part[$tp] = $ids;
|
||||
}
|
||||
|
||||
// Build conditions: text parts match Strasse.name OR pre-resolved gemeinde_id (uses index)
|
||||
$conditions = [];
|
||||
foreach ($text_parts as $tp) {
|
||||
$escaped = $db->escape($tp);
|
||||
if (!empty($gemeinde_ids_per_part[$tp])) {
|
||||
$ids_str = implode(',', $gemeinde_ids_per_part[$tp]);
|
||||
$conditions[] = "(Strasse.name LIKE '%$escaped%' OR Strasse.gemeinde_id IN ($ids_str))";
|
||||
} else {
|
||||
$conditions[] = "Strasse.name LIKE '%$escaped%'";
|
||||
}
|
||||
}
|
||||
if ($hausnummer !== null) {
|
||||
$hn = $db->escape($hausnummer);
|
||||
$conditions[] = "Hausnummer.hausnummer LIKE '$hn%'";
|
||||
}
|
||||
if ($plz !== null) {
|
||||
$pz = $db->escape($plz);
|
||||
$conditions[] = "Plz.plzstring LIKE '$pz%'";
|
||||
}
|
||||
|
||||
$where = count($conditions) ? implode(' AND ', $conditions) : '1=0';
|
||||
|
||||
$sql = "SELECT Strasse.name as strasse, Hausnummer.hausnummer, Plz.plz, Gemeinde.name as gemeinde
|
||||
FROM Strasse
|
||||
STRAIGHT_JOIN Hausnummer ON (Hausnummer.strasse_id = Strasse.id)
|
||||
LEFT JOIN Plz ON (Plz.id = Hausnummer.plz_id)
|
||||
LEFT JOIN Gemeinde ON (Gemeinde.id = Strasse.gemeinde_id)
|
||||
WHERE $where
|
||||
GROUP BY Strasse.name, Hausnummer.hausnummer, Plz.plz, Gemeinde.name
|
||||
ORDER BY Strasse.name, Hausnummer.hausnummer + 0, Plz.plz
|
||||
LIMIT 25";
|
||||
$results = [];
|
||||
$res = $db->query($sql);
|
||||
if ($db->num_rows($res)) {
|
||||
while ($row = $db->fetch_object($res)) {
|
||||
$text = $row->strasse;
|
||||
if ($row->hausnummer) $text .= ' ' . $row->hausnummer;
|
||||
if ($row->plz || $row->gemeinde) $text .= ', ' . trim($row->plz . ' ' . $row->gemeinde);
|
||||
$results[] = [
|
||||
"id" => $text,
|
||||
"text" => $text,
|
||||
"street" => $row->strasse,
|
||||
"number" => $row->hausnummer,
|
||||
"zip" => $row->plz,
|
||||
"city" => $row->gemeinde,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->returnJson(["results" => $results]);
|
||||
}
|
||||
}
|
||||
@@ -1451,13 +1451,14 @@ class PreorderController extends mfBaseController {
|
||||
$fcps = ADBRimoFcp::getAll(["netzgebiet_id" => intval($campaign->network->adb_netzgebiet_id)]) ?? [];
|
||||
if (empty($fcps)) return [];
|
||||
|
||||
$fcpIds = array_map(fn($fcp) => $fcp->id, $fcps);
|
||||
$stats = ADBRimoFcp::getRimoFcpStatistics($fcpIds);
|
||||
$statsMap = [];
|
||||
foreach ($stats as $stat) {
|
||||
$statsMap[$stat['fcp_id']] = $stat['total_active_preorders'];
|
||||
$filter = $this->request->filter ?? [];
|
||||
if (isset($filter['fcp'])) {
|
||||
unset($filter['fcp']);
|
||||
}
|
||||
|
||||
$filter = $this->getPreparedFilter($filter);
|
||||
$statsMap = PreorderModel::countActiveGroupedByFcp($filter);
|
||||
|
||||
$result = array_map(
|
||||
fn($fcp) => [
|
||||
"real_id" => $fcp->id,
|
||||
@@ -1465,12 +1466,12 @@ class PreorderController extends mfBaseController {
|
||||
"text" => $fcp->name ?? null,
|
||||
'lat' => $fcp->gps_lat ?? null,
|
||||
'lng' => $fcp->gps_long ?? null,
|
||||
'preorder_count' => $statsMap[$fcp->id] ?? 0
|
||||
'preorder_count' => $statsMap[$fcp->name] ?? 0
|
||||
],
|
||||
$fcps
|
||||
);
|
||||
|
||||
usort($result, fn($a, b) => $b['preorder_count'] <=> $a['preorder_count']);
|
||||
usort($result, fn($a, $b) => $b['preorder_count'] <=> $a['preorder_count']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -482,6 +482,41 @@ class PreorderModel
|
||||
return self::count($filter);
|
||||
}
|
||||
|
||||
public static function countActiveGroupedByFcp($filter = [])
|
||||
{
|
||||
if (!is_array($filter)) return false;
|
||||
|
||||
if (!array_key_exists("deleted", $filter)) {
|
||||
$filter["deleted"] = null;
|
||||
}
|
||||
|
||||
if (!array_key_exists("<status_code", $filter) && !array_key_exists(">status_code", $filter) && !array_key_exists("status_code", $filter)
|
||||
&& !array_key_exists("<status_id", $filter) && !array_key_exists(">status_id", $filter) && !array_key_exists("status_id", $filter)) {
|
||||
$filter["<status_code"] = 899;
|
||||
$filter["!status_code"] = 20;
|
||||
}
|
||||
|
||||
$db = FronkDB::singleton();
|
||||
$where = self::getSqlFilter($filter);
|
||||
$sql = "SELECT hn.rimo_fcp_name, COUNT(*) as cnt FROM `" . FRONKDB_DBNAME . "`.Preorder tt_preorder
|
||||
LEFT JOIN `" . FRONKDB_DBNAME . "`.Preorderstatus tt_preorderstatus ON (tt_preorder.status_id = tt_preorderstatus.id)
|
||||
LEFT JOIN `" . ADDRESSDB_DBNAME . "`.view_hausnummer as adb_hausnummer ON (tt_preorder.adb_hausnummer_id = adb_hausnummer.hausnummer_id)
|
||||
LEFT JOIN `" . ADDRESSDB_DBNAME . "`.Hausnummer as hn ON (adb_hausnummer.hausnummer_id = hn.id)
|
||||
LEFT JOIN `" . ADDRESSDB_DBNAME . "`.Wohneinheit as adb_wohneinheit ON (tt_preorder.adb_wohneinheit_id = adb_wohneinheit.id)
|
||||
WHERE $where AND hn.rimo_fcp_name IS NOT NULL
|
||||
GROUP BY hn.rimo_fcp_name
|
||||
";
|
||||
|
||||
$res = $db->query($sql);
|
||||
$counts = [];
|
||||
if ($db->num_rows($res)) {
|
||||
while ($row = $db->fetch_object($res)) {
|
||||
$counts[$row->rimo_fcp_name] = (int)$row->cnt;
|
||||
}
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filter
|
||||
* @param $limit
|
||||
|
||||
@@ -201,6 +201,17 @@ class WorkorderModel extends TTCrudBaseModel
|
||||
return $result ? $result->fetch_assoc()['count'] : 0;
|
||||
}
|
||||
|
||||
public static function getPreorderIdsByCampaigns(array $campaignIds): array {
|
||||
if (empty($campaignIds)) return [];
|
||||
$db = self::getDB();
|
||||
$campaignIdList = implode(',', array_map('intval', $campaignIds));
|
||||
$result = $db->query("SELECT id FROM `" . FRONKDB_DBNAME . "`.`Preorder` WHERE preordercampaign_id IN ($campaignIdList)");
|
||||
if (!$result || $result->num_rows === 0) return [];
|
||||
$ids = array_column($result->fetch_all(MYSQLI_ASSOC), 'id');
|
||||
$result->free();
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public static function getTechnicalData(int $workorderId): ?array {
|
||||
$workorder = self::get($workorderId);
|
||||
if (!$workorder || !$workorder->preorderId) return null;
|
||||
|
||||
@@ -253,12 +253,8 @@ class WorkorderBaseController extends TTCrud
|
||||
|
||||
$statusesToCheck = ['new', 'assigned', 'scheduled', 'in_progress', 'correction_requested', 'intervention_required', 'civil_engineering_required', 'civil_engineering_completed', 'problem_solved'];
|
||||
|
||||
// Get ALL preorders for tenant (including deleted/cancelled) to ensure their workorders get archived
|
||||
// Note: Not passing 'deleted' filter means all preorders are returned regardless of deleted status
|
||||
$allTenantPreorders = PreorderModel::search(['preordercampaign_id' => $tenantCampaignIds]);
|
||||
if(empty($allTenantPreorders)) continue;
|
||||
|
||||
$allTenantPreorderIds = array_map(fn($p) => $p->id, $allTenantPreorders);
|
||||
$allTenantPreorderIds = WorkorderModel::getPreorderIdsByCampaigns($tenantCampaignIds);
|
||||
if (empty($allTenantPreorderIds)) continue;
|
||||
|
||||
$workordersToCheck = WorkorderModel::getAll([
|
||||
'status' => $statusesToCheck,
|
||||
|
||||
@@ -1474,6 +1474,31 @@ $(document).ready(function () {
|
||||
{
|
||||
$('#customer-info-type').val('1').trigger('change');
|
||||
}
|
||||
if (localsorageEvent.calendar_user_name) {
|
||||
const calUserName = localsorageEvent.calendar_user_name.toLowerCase();
|
||||
const calUserOption = $('#calendar-users option').filter(function () {
|
||||
return $(this).text().trim().toLowerCase().includes(calUserName);
|
||||
}).first();
|
||||
if (calUserOption.length) {
|
||||
$('#calendar-users').val(calUserOption.val()).trigger('change');
|
||||
}
|
||||
}
|
||||
if (localsorageEvent.attendee_names && localsorageEvent.attendee_names.length) {
|
||||
const attendeeValues = [];
|
||||
localsorageEvent.attendee_names.forEach(function (attendeeName) {
|
||||
const nameLower = attendeeName.toLowerCase();
|
||||
const opt = $('#calendar-attendees option').filter(function () {
|
||||
return $(this).text().trim().toLowerCase().includes(nameLower);
|
||||
}).first();
|
||||
if (opt.length) {
|
||||
attendeeValues.push(opt.val());
|
||||
} else {
|
||||
$('#calendar-attendees').append(new Option(attendeeName, attendeeName, true, true));
|
||||
attendeeValues.push(attendeeName);
|
||||
}
|
||||
});
|
||||
$('#calendar-attendees').val(attendeeValues).trigger('change');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -2353,6 +2378,8 @@ $(document).ready(function () {
|
||||
|
||||
$('body').on('change', '#customer,#customer-info-type', function () {
|
||||
|
||||
if (!$('#customer').hasClass('select2-hidden-accessible')) return;
|
||||
|
||||
if ($('#name').val() == "") {
|
||||
$('#name').val('IBN: ' + $("#customer").select2('data')[0].name);
|
||||
}
|
||||
|
||||
@@ -64,16 +64,19 @@ Vue.component('workorder-admin', {
|
||||
</template>
|
||||
|
||||
<template v-slot:additionalinfo="{ row }">
|
||||
<div v-if="editingAdditionalInfoId === row.id">
|
||||
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" rows="3" no-form-group sm ref="editTextarea"/>
|
||||
<div v-if="editingAdditionalInfoId === row.id" class="wo-notiz-edit">
|
||||
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" :rows="editTextareaRows" no-form-group sm ref="editTextarea"/>
|
||||
<div class="mt-2 d-flex justify-content-end">
|
||||
<tt-button text="Abbrechen" @click="cancelEdit" sm additional-class="btn-secondary mr-2"/>
|
||||
<tt-button text="Speichern" @click="updateAdditionalInfo(row)" sm additional-class="btn-success"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-start">
|
||||
<span style="white-space: pre-wrap; max-width: 250px; display: inline-block;">{{ row.additionalInfo || '-' }}</span>
|
||||
<tt-button icon="fas fa-edit" @click="startAdditionalInfoEdit(row)" additional-class="btn-link btn-sm p-0 ml-2" title="Zusatz-Info bearbeiten"/>
|
||||
<div v-else>
|
||||
<div :class="['wo-notiz-text', expandedNotes.includes(row.id) ? 'wo-notiz-expanded' : '']">{{ row.additionalInfo || '-' }}</div>
|
||||
<div class="wo-notiz-actions">
|
||||
<button v-if="isNoteLong(row.additionalInfo)" class="wo-notiz-toggle" @click="toggleNoteExpand(row.id)">{{ expandedNotes.includes(row.id) ? '▴ weniger' : '▾ mehr' }}</button>
|
||||
<a class="wo-notiz-edit-btn" @click="startAdditionalInfoEdit(row)" title="Zusatz-Info bearbeiten"><i class="fas fa-edit"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -122,7 +125,7 @@ Vue.component('workorder-admin', {
|
||||
data() {
|
||||
return {
|
||||
window, workordersToAssign: [], editingWorkorderId: null, editingDeadlineId: null, editingAdditionalInfoId: null,
|
||||
civilEngineeringData: null, tempAdditionalInfo: '', companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null,
|
||||
civilEngineeringData: null, tempAdditionalInfo: '', expandedNotes: [], companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null,
|
||||
cancelWorkorderModalData: null, problemSolvedModalData: null, massAssignModalData: null,
|
||||
crudConfig: {
|
||||
...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true,
|
||||
@@ -155,7 +158,11 @@ Vue.component('workorder-admin', {
|
||||
const firstWorkorder = rows.find(r => r.id === this.workordersToAssign[0]);
|
||||
if (!firstWorkorder) return [];
|
||||
return this.companiesByTenant[firstWorkorder.tenantId] || [];
|
||||
}
|
||||
},
|
||||
editTextareaRows() {
|
||||
const lines = (this.tempAdditionalInfo || '').split('\n').length;
|
||||
return String(Math.max(4, lines + 1));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async chargeWorkorder(row) {
|
||||
@@ -273,6 +280,15 @@ Vue.component('workorder-admin', {
|
||||
this.problemSolvedModalData = null;
|
||||
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
},
|
||||
isNoteLong(text) {
|
||||
if (!text) return false;
|
||||
return text.split('\n').length > 4 || text.length > 180;
|
||||
},
|
||||
toggleNoteExpand(rowId) {
|
||||
const idx = this.expandedNotes.indexOf(rowId);
|
||||
if (idx === -1) this.expandedNotes.push(rowId);
|
||||
else this.expandedNotes.splice(idx, 1);
|
||||
},
|
||||
startAdditionalInfoEdit(row) {
|
||||
this.editingAdditionalInfoId = row.id;
|
||||
this.tempAdditionalInfo = row.additionalInfo || '';
|
||||
|
||||
@@ -41,4 +41,58 @@
|
||||
|
||||
.workorder-button {
|
||||
padding: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.wo-notiz-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 250px;
|
||||
max-height: calc(5 * 1.5em);
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.wo-notiz-text.wo-notiz-expanded {
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.wo-notiz-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.wo-notiz-toggle {
|
||||
font-size: 0.8rem;
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wo-notiz-toggle:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wo-notiz-edit-btn {
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wo-notiz-edit-btn:hover {
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.wo-notiz-edit {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.wo-notiz-edit textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -35,18 +35,20 @@ Vue.component('workorder-company', {
|
||||
</template>
|
||||
|
||||
<template v-slot:additionalinfo="{ row }">
|
||||
<div v-if="editingAdditionalInfoId === row.id">
|
||||
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" rows="3" no-form-group sm ref="editTextarea"/>
|
||||
<div v-if="editingAdditionalInfoId === row.id" class="wo-notiz-edit">
|
||||
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" :rows="editTextareaRows" no-form-group sm ref="editTextarea"/>
|
||||
<div class="mt-2 d-flex justify-content-end">
|
||||
<tt-button text="Abbrechen" @click="cancelEdit" sm additional-class="btn-secondary mr-2"/>
|
||||
<tt-button text="Speichern" @click="updateAdditionalInfo(row)" sm additional-class="btn-success"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-start">
|
||||
<span style="white-space: pre-wrap; max-width: 250px; display: inline-block;">{{ row.additionalInfo || '-' }}</span>
|
||||
<tt-button v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
|
||||
icon="fas fa-edit" @click="startAdditionalInfoEdit(row)"
|
||||
additional-class="btn-link btn-sm p-0 ml-2" title="Zusatz-Info bearbeiten"/>
|
||||
<div v-else>
|
||||
<div :class="['wo-notiz-text', expandedNotes.includes(row.id) ? 'wo-notiz-expanded' : '']">{{ row.additionalInfo || '-' }}</div>
|
||||
<div class="wo-notiz-actions">
|
||||
<button v-if="isNoteLong(row.additionalInfo)" class="wo-notiz-toggle" @click="toggleNoteExpand(row.id)">{{ expandedNotes.includes(row.id) ? '▴ weniger' : '▾ mehr' }}</button>
|
||||
<a v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
|
||||
class="wo-notiz-edit-btn" @click="startAdditionalInfoEdit(row)" title="Zusatz-Info bearbeiten"><i class="fas fa-edit"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,6 +106,7 @@ Vue.component('workorder-company', {
|
||||
rescheduleModalData: null,
|
||||
editingAdditionalInfoId: null,
|
||||
tempAdditionalInfo: '',
|
||||
expandedNotes: [],
|
||||
crudConfig: {
|
||||
...window.TT_CONFIG.CRUD_CONFIG,
|
||||
expandable: true,
|
||||
@@ -121,6 +124,12 @@ Vue.component('workorder-company', {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editTextareaRows() {
|
||||
const lines = (this.tempAdditionalInfo || '').split('\n').length;
|
||||
return String(Math.max(4, lines + 1));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getStatusColumn(status) {
|
||||
const column = this.crudConfig.columns.find(c => c.key === 'status');
|
||||
@@ -130,19 +139,66 @@ Vue.component('workorder-company', {
|
||||
if (!timestamp) return '–';
|
||||
return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY');
|
||||
},
|
||||
getCalendarType(networkOwnerName) {
|
||||
if (!networkOwnerName) return '1';
|
||||
const name = networkOwnerName.toLowerCase();
|
||||
if (name.includes('xinon')) return '2';
|
||||
if (name.includes('sbidi')) return '7';
|
||||
if (name.includes('estmk')) return '3';
|
||||
return '1';
|
||||
},
|
||||
getCampaignName(preordercampaignId) {
|
||||
const col = this.crudConfig.columns.find(c => c.key === 'preordercampaign_id');
|
||||
const opt = col?.table?.filterOptions?.find(o => o.value == preordercampaignId);
|
||||
return opt?.text || '';
|
||||
},
|
||||
openCalendarWithPrefill(workorder, dateUnix, win) {
|
||||
const m = window.moment.unix(dateUnix);
|
||||
const zeitraum = m.hour() < 12 ? 'VM' : 'NM';
|
||||
const campaignName = this.getCampaignName(workorder.preordercampaign_id);
|
||||
const locationParts = [workorder.street, workorder.hausnummer];
|
||||
if (workorder.stiege) locationParts.push('/' + workorder.stiege);
|
||||
const location = locationParts.join(' ') + ', ' + workorder.plz + ' ' + workorder.city;
|
||||
const descLines = [];
|
||||
if (workorder.oaid || campaignName || workorder.city) {
|
||||
descLines.push([workorder.oaid, campaignName, workorder.city].filter(Boolean).join(' - '));
|
||||
}
|
||||
descLines.push('Zeitraum: ' + zeitraum);
|
||||
if (workorder.phone) descLines.push('Tel.: ' + workorder.phone);
|
||||
if (workorder.additionalInfo) descLines.push(workorder.additionalInfo);
|
||||
const calendarData = {
|
||||
type: this.getCalendarType(workorder.networkOwnerName),
|
||||
subject: workorder.customerCompany || workorder.customerName || '',
|
||||
location: location,
|
||||
cstart: m.format('DD.MM.YYYY HH:mm'),
|
||||
cend: m.clone().add(90, 'minutes').format('DD.MM.YYYY HH:mm'),
|
||||
description: descLines.join('<br>'),
|
||||
customer_phone: workorder.phone || null,
|
||||
customer_email: workorder.email || null,
|
||||
calendar_user_name: 'Pusnik',
|
||||
attendee_names: ['Ziga Harc'],
|
||||
};
|
||||
localStorage.setItem('Calendar_create', JSON.stringify(calendarData));
|
||||
win.location.href = '/Calendar/View';
|
||||
},
|
||||
async setAppointment(workorder, date) {
|
||||
if (!date) return;
|
||||
if (moment.unix(date).hour() >= 23 || moment.unix(date).hour() < 1) {
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||||
}
|
||||
const calWin = window.open('about:blank', '_blank');
|
||||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/scheduleAppointment`, {
|
||||
workorderId: workorder.id, appointmentDate: date
|
||||
});
|
||||
if (data.success) {
|
||||
window.notify('success', data.message);
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
this.openCalendarWithPrefill(workorder, date, calWin);
|
||||
} else {
|
||||
if (calWin) calWin.close();
|
||||
window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
}
|
||||
},
|
||||
openRescheduleModal(row) {
|
||||
this.rescheduleModalData = { workorder: row, newDate: row.appointmentDate, reason: '' };
|
||||
@@ -151,14 +207,19 @@ Vue.component('workorder-company', {
|
||||
const { workorder, newDate, reason } = this.rescheduleModalData;
|
||||
if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.');
|
||||
if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||||
const calWin = window.open('about:blank', '_blank');
|
||||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/rescheduleAppointment`, {
|
||||
workorderId: workorder.id, appointmentDate: newDate, reason: reason
|
||||
});
|
||||
if (data.success) {
|
||||
window.notify('success', data.message);
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
this.openCalendarWithPrefill(workorder, newDate, calWin);
|
||||
this.rescheduleModalData = null;
|
||||
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
} else {
|
||||
if (calWin) calWin.close();
|
||||
window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
}
|
||||
},
|
||||
async clearAppointment(workorder) {
|
||||
if (!confirm('Möchten Sie den Termin wirklich löschen?')) return;
|
||||
@@ -170,6 +231,15 @@ Vue.component('workorder-company', {
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||||
},
|
||||
isNoteLong(text) {
|
||||
if (!text) return false;
|
||||
return text.split('\n').length > 4 || text.length > 180;
|
||||
},
|
||||
toggleNoteExpand(rowId) {
|
||||
const idx = this.expandedNotes.indexOf(rowId);
|
||||
if (idx === -1) this.expandedNotes.push(rowId);
|
||||
else this.expandedNotes.splice(idx, 1);
|
||||
},
|
||||
startAdditionalInfoEdit(row) {
|
||||
this.editingAdditionalInfoId = row.id;
|
||||
this.tempAdditionalInfo = row.additionalInfo || '';
|
||||
|
||||
Reference in New Issue
Block a user