Merge branch 'master' into 'fronkdev'

# Conflicts:
#   application/Preorder/PreorderController.php
This commit is contained in:
Frank Schubert
2025-09-17 10:17:18 +00:00
123 changed files with 9982 additions and 3945 deletions
+71 -25
View File
@@ -171,7 +171,14 @@
<label class="form-label" for="filter_home_oaid_rimo_id">Home OAID / Rimo ID</label>
<input type="text" class="form-control" name="filter[home_oaid_rimo_id]" id="filter_home_oaid_rimo_id" value="<?=(array_key_exists("home_oaid_rimo_id", $filter)) ? $filter['home_oaid_rimo_id'] : ""?>" />
</div>
<div class="col-sm-12 col-md-1">
<label class="form-label" for="filter_fcp">FCP</label>
<select name="filter[rimo_fcp_name][]" id="filter_fcp" multiple class="form-control">
<option value="">Kein FCP gefunden</option>
</select>
</div>
</div>
<div class="row mt-2">
<div class="col">
@@ -225,6 +232,7 @@
<th>Straße</th>
<th>Hausnr.</th>
<th>Stiege</th>
<th>FCP</th>
<th>Homes/<wbr>Preorders</th>
<th>Rimo-ID</th>
<th>Rollout Jahr</th>
@@ -244,6 +252,7 @@
<td><?=$address->strasse->name?></td>
<td><?=$address->hausnummer?></td>
<td><?=$address->stiege?></td>
<td><?=$address->rimo_fcp_name ?? 'N/A'?></td>
<td><?=count($address->wohneinheiten)?>
<span class="text-secondary" title="<?=($address->tool_building_type == 0) ? "Unbekannt" : (($address->tool_building_type == 1) ? "EFH" : "MPH")?>">
<i class="fas fa-fw <?=($address->tool_building_type == 0) ? "fa-question" : (($address->tool_building_type == 1) ? "fa-home" : "fa-building")?>"></i>
@@ -276,29 +285,66 @@
</div>
</div>
<script>
$("#filter_status_id").select2({closeOnSelect: false});
$("#filter_status_flag").select2({closeOnSelect: false});
$("#filter_network_id").select2({closeOnSelect: false});
<script>
$(document).ready(function() {
$("#filter_status_id, #filter_status_flag, #filter_network_id").select2({ closeOnSelect: false });
$('#filter_network_id').change(function() {
if($('#filter_network_id').val() === "null") {
$('#filter-gemeinde-id').hide();
$('#filter-gemeinde-text').show();
$('#filter-ortschaft-id').hide();
$('#filter-ortschaft-text').show();
$('#filter-gemeinde-id option:first').prop("selected", "selected");
$('#filter-ortschaft-id option:first').prop("selected", "selected");
} else {
$('#filter-gemeinde-text').hide();
$('#filter-gemeinde-id').show();
$('#filter-ortschaft-text').hide();
$('#filter-ortschaft-id').show();
}
$('#filter_gemeinde').val("");
$('#filter_ortschaft').val("");
});
</script>
const fcpSelect = $("#filter_fcp");
const networkSelect = $("#filter_network_id");
const apiUrl = "<?=self::getUrl("AddressDB", "api")?>";
const updateFcpSelect = (placeholder, data = []) => {
fcpSelect.empty().select2({ data, placeholder, allowClear: true });
};
updateFcpSelect("Bitte ein Netzgebiet auswählen");
networkSelect.on('change', function() {
const selectedNets = $(this).val() || [];
const hasNull = Array.isArray(selectedNets) && selectedNets.includes("null");
$('#filter-gemeinde-text, #filter-ortschaft-text').toggle(hasNull);
$('#filter-gemeinde-id, #filter-ortschaft-id').toggle(!hasNull);
$('#filter_gemeinde, #filter_ortschaft').val("");
if (hasNull) {
$('#filter-gemeinde-id, #filter-ortschaft-id').find('option:first').prop("selected", "selected");
}
if (selectedNets.length !== 1) {
updateFcpSelect(selectedNets.length > 1 ? "Bitte genau ein Netzgebiet auswählen" : "Kein Netzgebiet ausgewählt");
return;
}
const networkId = selectedNets[0];
if (networkId === 'null') {
updateFcpSelect("Kein Netzgebiet ausgewählt");
return;
}
$.get(apiUrl, { do: "getFCPsForNetwork", network_id: networkId }, (response) => {
if (response?.status === "OK" && Array.isArray(response.result)) {
let fcpData = response.result;
fcpData.unshift({ id: "", text: "" });
fcpData.sort((a, b) => {
const aN = a.text.replace(/\D/g, ""), bN = b.text.replace(/\D/g, "");
return aN && bN ? parseInt(aN, 10) - parseInt(bN, 10) : a.text.localeCompare(b.text);
});
updateFcpSelect("FCP auswählen", fcpData);
const fcpValues = new URLSearchParams(window.location.search).getAll("filter[rimo_fcp_name][]");
if (fcpValues.length > 0) {
fcpSelect.val(fcpValues).trigger("change");
}
} else {
updateFcpSelect("Keine FCPs gefunden");
}
}, "json").fail(() => {
updateFcpSelect("Fehler beim Laden");
});
}).trigger('change');
});
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
+14 -5
View File
@@ -48,9 +48,12 @@
<th>Extref</th>
<td><?=$address->extref?></td>
</tr><tr>
<th>Rimo External ID</th>
<td><?=$address->rimo_id?></td>
</tr><tr>
<th>Rimo External ID</th>
<td><?=$address->rimo_id?></td>
</tr><tr>
<th>Rimo Type</th>
<td><?=$address->rimo_type?></td>
</tr><tr>
<th>Netzgebiet</th>
<td><?=$address->netzgebiet->name?></td>
</tr><tr>
@@ -176,7 +179,10 @@
</tr>
<?php foreach($address->wohneinheiten as $unit): ?>
<tr>
<td><a href="<?=self::getUrl("ADBWohneinheit", "edit", ["id" => $unit->id])?>"><i class="fas fa-edit"></i></a></td>
<td>
<a href="#" data-home-id="<?=$unit->id?>" data-home-contact title="Kontakte bearbeiten"><i class="fas fa-users-cog text-primary"></i></a>
<a href="<?=self::getUrl("ADBWohneinheit", "edit", ["id" => $unit->id])?>"><i class="fas fa-edit"></i></a>
</td>
<td><?=$unit->id?></td>
<td class="text-pink">
<?php if($unit->oaid): ?>
@@ -388,4 +394,7 @@
'json');
}
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
<script src="<?= self::getResourcePath() ?>js/pages/AddressDB/ADBWohneinheitContactManager.js"></script>
<script src="<?= self::getResourcePath() ?>plugins/axios/axios.min.js"></script>
<script src="<?= self::getResourcePath() ?>plugins/axios/axios.inject.js"></script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
@@ -483,13 +483,35 @@ foreach ($owners as $owner):
</tr>
</table>
<div class="signature-line" style="margin-top: 128px">
<div class="float-left" style="width: 25%;">Ort, Datum</div>
<div class="float-right" style="width: 75%;">
<strong><?= ($owner->title) ? $owner->title . " " : "" ?><?= $owner->company ? $owner->company : $owner->firstname . ' ' . $owner->lastname ?></strong>
<br>Unterschrift mit Geburtsdatum bzw. firmenmäßige Zeichnung des/r Liegenschaftseigentümer(s)
<?php if ($owner->signature): ?>
<table style="width: 100%; margin-top: 80px; border-collapse: collapse; page-break-inside: avoid;">
<tr>
<td style="width: 33%; vertical-align: bottom; border-bottom: 1px solid #000; padding-bottom: 2px;">
<?php if ($owner->signature_date): ?>
<span style="font-size: 9px;">Graz, <?= date("d.m.Y", $owner->signature_date) ?></span>
<?php endif; ?>
</td>
<td style="width: 67%; vertical-align: bottom; border-bottom: 1px solid #000; padding-bottom: 2px; text-align: center;">
<img src="<?= $owner->signature ?>" style="max-height: 60px; max-width: 250px;" />
</td>
</tr>
<tr>
<td style="font-size: 9px; padding-top: 4px;">Ort, Datum</td>
<td style="font-size: 9px; padding-top: 4px; text-align: center;">
<strong><?= $owner->signature_name ?></strong>
<br>Unterschrift bzw. firmenmäßige Zeichnung des/r Liegenschaftseigentümer(s)
</td>
</tr>
</table>
<?php else: ?>
<div class="signature-line" style="margin-top: 128px; page-break-inside: avoid;">
<div class="float-left" style="width: 25%;">Ort, Datum</div>
<div class="float-right" style="width: 75%;">
<strong><?= ($owner->title) ? $owner->title . " " : "" ?><?= $owner->company ? $owner->company : $owner->firstname . ' ' . $owner->lastname ?></strong>
<br>Unterschrift mit Geburtsdatum bzw. firmenmäßige Zeichnung des/r Liegenschaftseigentümer(s)
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</body>
+1 -1
View File
@@ -216,7 +216,7 @@
<div class="card">
<h5 class="card-header">Oder Plan hochladen</h5>
<div class="card-body">
<input type="file" class="form-control" name="consent_plan_image" id="consent_plan_image" />
<input type="file" class="form-control" name="consent_plan_image" id="consent_plan_image" accept="image/png, image/jpeg, image/jpg" />
</div>
</div>
+8 -4
View File
@@ -212,10 +212,14 @@ $pagination_entity_name = "Adressen";
</tr><tr>
<th>Plan/Skizze</th>
<td>
<?php if($item->file && $item->file->file && $item->file->file->fileExists()): ?>
<!--img src="<?=self::getUrl("File", "Download", ["id" => $item->file->file_id])?>" style="max-width: 480px;"/-->
<img src="<?=$item->file->file->asDataUrl()?>" style="max-width: 480px;" />
<?php endif; ?>
<?php if($item->file && $item->file->file && $item->file->file->fileExists()):
$dataUrl = $item->file->file->asDataUrl();
if (str_contains($dataUrl, 'application/pdf')) {
echo '<a href="' . $dataUrl . '" download="your-file-name.pdf" class="btn btn-primary" aria-label="Download PDF"><i class="fas fa-download"></i> Download PDF</a>';
} else {
echo '<img src="' . $dataUrl . '" style="max-width: 480px;" alt="File preview"/>';
}
endif; ?>
</td>
</tr><tr>
<th></th>
+6 -6
View File
@@ -1,10 +1,10 @@
<?php
$maxLength = max(mb_strlen($firstline ?? ''), mb_strlen($secondline ?? ''));
$maxLength = max(mb_strlen($firstline ?? ''), mb_strlen($secondline ?? ''), mb_strlen($thirdline ?? ''));
$fontSize = '12px';
if ($maxLength <= 15) $fontSize = '24px';
elseif ($maxLength <= 24) $fontSize = '18px';
elseif ($maxLength <= 50) $fontSize = '16px';
$fontSize = '13px';
if ($maxLength <= 11) $fontSize = '28px';
elseif ($maxLength <= 20) $fontSize = '18px';
elseif ($maxLength <= 45) $fontSize = '16px';
$this->setReturnValue(['filename' => "xyz." . time() . "pdf"]);
?>
@@ -42,4 +42,4 @@ $this->setReturnValue(['filename' => "xyz." . time() . "pdf"]);
<div><?= $fourthline ?></div>
</div>
</body>
</html>
</html>
+17
View File
@@ -2,6 +2,12 @@
<link href="<?=self::getResourcePath()?>assets/css/select2-cstm.css?<?=date('U')?>" rel="stylesheet" type="text/css" />
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet" type="text/css"/>
<!-- start page title -->
<style type="text/css">
.tool-border-spacer
{
border-right: 2px solid #868686;
}
</style>
<div class="row">
<div class="col-12">
<div class="page-title-box">
@@ -78,7 +84,18 @@
value="<?= $devicetypes->power ?>">
</div>
</div>
<div class="form-group row">
<label class="col-lg-2 col-form-label" for="price">Temperatur Warnung | Kritisch</label>
<div class="col-lg-2 tool-border-spacer">
<input type="number" min="0" step="1" class="form-control" name="temp_warning" id="temp_warning" placeholder="80"
value="<?= $devicetypes->temp_warning ?>" >
</div>
<div class="col-lg-2">
<input type="number" min="0" step="1" class="form-control" name="temp_critical" id="temp_critical" placeholder="90"
value="<?= $devicetypes->temp_critical ?>" >
</div>
</div>
</div>
+15 -3
View File
@@ -97,6 +97,12 @@
background-color: #d7d7d7;
opacity: 1;
}
.switch-rack-side {
margin-right: 8px;
margin-top: 2px;
cursor: pointer;
}
</style>
<div class="row">
<div class="col-12">
@@ -339,6 +345,7 @@ if (!empty(trim($pops->vlan_ipv6)))
<div class="col-lg-1"></div>
<label class="col-lg-4 col-form-label" for="module-slot">19 Zoll Position</label>
<div class="col-lg-3">
<input type="hidden" value="front" id="module-side" name="module-side"/>
<select required="required" id="module-slot" name="module-slot"
class="form-control">
<option value="1">1</option>
@@ -394,6 +401,7 @@ if (!empty(trim($pops->vlan_ipv6)))
<div class="col-lg-6">
<select required="required" id="module-ports" name="module-ports"
class="form-control">
<option value="96" data-plugs="1;2">96</option>
<option selected="selected" value="48" data-plugs="1;2">48</option>
<option value="24" data-plugs="2;3">24</option>
<option value="12" data-plugs="2;3">12</option>
@@ -483,13 +491,15 @@ if (!empty(trim($pops->vlan_ipv6)))
data-rackhe="<?= $poprack['rack']['he'] ?>"
data-rackid="<?= $poprack['rack']['id']; ?>"><span
class="rack-name"><i
class="fa-regular fa-arrows-up-down-left-right move-handle float-left"></i><?= $poprack['rack']['name']; ?></span>
class="fa-regular fa-arrows-up-down-left-right move-handle float-left"></i><?= $poprack['rack']['name']; ?>&nbsp;<span class="rack-side-indicator font-weight-normal">-&nbsp;Vorderseite</span></span>
<i class="fas fa-sync-alt float-right switch-rack-side" title="Seite wechseln"></i>
<i class="far fa-edit float-right" title="Bearbeiten"
data-toggle="modal" data-target="#rackModal"></i>
</th>
</tr>
</thead>
<tbody>
<tbody id="rack-body-<?= $poprack['rack']['id'] ?>" data-side="front">
<?php
$cellwidth = 227;
$blocktd = 0;
@@ -499,7 +509,8 @@ if (!empty(trim($pops->vlan_ipv6)))
data-toggle="modal" data-target="#rackModuleModal"
style="cursor: pointer" data-he="<?= $i; ?>">He<?= $i; ?></td>
<?php
foreach ($poprack['modules'] as $module) {
$modules_to_render = $poprack['modules']['front'] ?? [];
foreach ($modules_to_render as $module) {
if ($module['start_he'] == $i) {
$modulestart = 1;
@@ -511,6 +522,7 @@ if (!empty(trim($pops->vlan_ipv6)))
$extText = "";
$extTextspan = "";
foreach ($module['slots'] as $slots) {
var_dump();
$extText = "";
$title = $slots['modulname'];
if ($slots['type'] == '0') {
+85 -11
View File
@@ -24,7 +24,7 @@ $pagination_entity_name = "Vorbestellungen";
}
.preorder-campaign-header-buttons {
max-width: 900px;
max-width: 1100px;
}
.tr-highlight {
@@ -458,6 +458,8 @@ $pagination_entity_name = "Vorbestellungen";
</ul>
</div>
<a id="rimo-types-link" target="_blank" style="display:none" href="#" class="btn btn-outline-success"><i class="fas fa-map-marked-alt"></i>Rimo-Typen Karte anzeigen</a>
</div>
</div>
</form>
@@ -635,6 +637,7 @@ $pagination_entity_name = "Vorbestellungen";
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
<div class="preorder-campaign-table-actions">
<?php if(!$me->is(["preorderfront"]) && !$me->is("preorderreadonly")): ?>
<a href="#" data-home-id="<?=$preorder->adb_wohneinheit_id?>" data-home-contact title="Kontakte bearbeiten"><i class="fas fa-users-cog text-primary"></i></a>
<a href="<?=self::getUrl("Preorder", "edit", ["id" => $preorder->id])?>"><i class="far fa-edit" title="Vorbestellung Bearbeiten"></i></a>
<a href="<?=self::getUrl("Preorder", "delete", ["id" => $preorder->id, "filter" => $filter])?>" class="text-danger" onclick="if(!confirm('Vorbestellung wirklich löschen?')) return false;" title="Vorbestellung Löschen"><i class="fas fa-trash"></i></a>
<?php endif; ?>
@@ -1085,20 +1088,73 @@ $pagination_entity_name = "Vorbestellungen";
}
async function getFCPs(map) {
var fcp = await $.get("<?=self::getUrl("Preorder", "Api")?>", {
const fcpResponse = await $.get("<?=self::getUrl("Preorder", "Api")?>", {
do: "getFCPsForCampaign",
campaign_id: "<?=$campaign->id?>"
});
if(fcp.status == "OK") {
fcp.result.forEach((fcp) => {
var icon = L.MakiMarkers.icon({icon: "viewpoint", color: "yellow", size: "m"});
var marker = L.marker([fcp.lat, fcp.lng], {icon: icon}).addTo(map);
var google_maps_link = "https://www.google.com/maps/search/?api=1&query=" + fcp.lat + "," + fcp.lng;
var popup_content = "<a href='" + google_maps_link + "' target='_blank'>Google Maps</a><br />" + fcp.text;
marker.bindPopup(popup_content);
});
}
if (fcpResponse.status !== "OK" || !fcpResponse.result?.length) return;
const fcpIds = fcpResponse.result.map(fcp => fcp.real_id);
const statsResponse = await $.ajax({
url: "<?=self::getUrl("Preorder", "Api")?>?do=getRimoFcpStats",
type: 'POST',
contentType: 'application/json', // 1. Set the content type to JSON
data: JSON.stringify({ fcp_ids: fcpIds }) // 2. Stringify the data object
});
const stats = statsResponse.status === "OK" ? statsResponse.result : [];
fcpResponse.result.forEach(fcp => {
const icon = L.MakiMarkers.icon({ icon: "viewpoint", color: "yellow", size: "m" });
const marker = L.marker([fcp.lat, fcp.lng], { icon }).addTo(map);
const fcpStat = stats.find(s => parseInt(s.fcp_id) === parseInt(fcp.real_id));
const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${fcp.lat},${fcp.lng}`;
const statsHtml = !fcpStat ? `<p>Keine Statistiken gefunden.</p>` : `
<div style="margin-bottom: 15px;">
<strong style="display: block; margin-bottom: 5px; color: #555;">Zusammenfassung:</strong>
<span>Buildings: <b>${fcpStat.total_hausnummer_count}</b></span><br>
<span>Homes: <b>${fcpStat.total_wohneinheit_count}</b></span><br>
<span>Bestellungen: <b>${fcpStat.total_active_preorders}</b></span>
</div>
<strong style="display: block; margin-bottom: 5px; color: #555;">Details nach RIMO-Typ:</strong>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr style="background-color: #f2f2f2; text-align: left;">
<th style="padding: 8px; border: 1px solid #ddd;">Typ</th>
<th style="padding: 8px; border: 1px solid #ddd;">BU</th>
<th style="padding: 8px; border: 1px solid #ddd;">WE</th>
<th style="padding: 8px; border: 1px solid #ddd;">BE</th>
</tr>
</thead>
<tbody>
${Object.entries(fcpStat.counts_by_rimo_type || {}).length ?
Object.entries(fcpStat.counts_by_rimo_type).map(([type, counts], index) => `
<tr style="${index % 2 === 0 ? 'background-color: #ffffff;' : 'background-color: #f9f9f9;'}">
<td style="padding: 8px; border: 1px solid #ddd;">${type}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${counts.hausnummer_count}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${counts.wohneinheit_count}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${counts.preorder_count}</td>
</tr>
`).join('') :
'<tr><td colspan="4" style="padding: 8px; text-align: center; border: 1px solid #ddd;">Keine detaillierten Statistiken verfügbar.</td></tr>'
}
</tbody>
</table>
`;
const popupContent = `
<div style="font-family: Arial, sans-serif; width: 320px; padding: 5px;">
<h3 style="margin-bottom: 10px; color: #333; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
${fcp.text}
</h3>
<a href='${googleMapsLink}' target='_blank' style="color: #007bff; text-decoration: none; margin-bottom: 15px; display: inline-block;">In Google Maps anzeigen</a>
${statsHtml}
</div>
`;
marker.bindPopup(popupContent);
});
}
function centerMap() {
@@ -1967,6 +2023,24 @@ $pagination_entity_name = "Vorbestellungen";
});
});
campaignSelect.trigger("change");
// for the Rimo-Typen Karte <a> only show this <a> button if a preordercampaign is selected and change the display and href dynamically
const rimoTypesLink = $("#rimo-types-link");
function updateRimoTypesLink() {
const campaignId = campaignSelect.val();
if (campaignId) {
rimoTypesLink.show();
rimoTypesLink.attr("href", "<?=self::getUrl("Preorder", "RimoTypeMap")?>?preordercampaign_id=" + campaignId);
} else {
rimoTypesLink.hide();
rimoTypesLink.attr("href", "#");
}
}
campaignSelect.on("change", updateRimoTypesLink);
updateRimoTypesLink();
});
</script>
<script src="<?= self::getResourcePath() ?>js/pages/AddressDB/ADBWohneinheitContactManager.js"></script>
<script src="<?= self::getResourcePath() ?>plugins/axios/axios.min.js"></script>
<script src="<?= self::getResourcePath() ?>plugins/axios/axios.inject.js"></script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
+9 -3
View File
@@ -16,7 +16,7 @@ foreach(PreorderStatusflagModel::getAll() as $sflag) {
}
?>
<?="\u{FEFF}"?>Kampagne;Netzgebiet ID;Netzgebiet;Extref;Bestellcode;Gutscheincodes;OAID;Bestelldatum;Bestelltyp;Status Code;Status Name;ADB NE;"<?=implode('";"', $status_flags_header)?>";Anschlusstyp;GWR Adresscode;Meridian;RW;HW;Anschluss Strasse;Anschluss Hausnummer;Anschluss PLZ;Anschluss Ort;Anschluss Wohneinheit;GPS Breite;GPS Länge;Anzahl Anschlüsse;Kunde Firma;Kunde UID;Kunde Vorname;Kunde Nachname;Kunde Strasse;Kunde PLZ;Kunde Ort;Kunde Telefon;Kunde Email;Partner;CIF Token;Cif Url;Cif Cable Url;Addon Lehrverrohrung Grundstück;Addon Hausverkabelung;BEP festgelegt;Starterpaket erhalten;Erstellt;Letzte Bearbeitung
<?="\u{FEFF}"?>Kampagne;Netzgebiet ID;Netzgebiet;Extref;Bestellcode;Gutscheincodes;OAID;FCP;Bestelldatum;Bestelltyp;Status Code;Status Name;ADB NE;"<?=implode('";"', $status_flags_header)?>";Anschlusstyp;GWR Adresscode;Meridian;RW;HW;Anschluss Strasse;Anschluss Hausnummer;Anschluss PLZ;Anschluss Ort;Anschluss Wohneinheit;GPS Breite;GPS Länge;Anzahl Anschlüsse;Kunde Firma;Kunde UID;Kunde Vorname;Kunde Nachname;Kunde Strasse;Kunde PLZ;Kunde Ort;Kunde Telefon;Kunde Email;Partner;CIF Token;Cif Url;Cif Cable Url;Addon Lehrverrohrung Grundstück;Addon Hausverkabelung;BEP festgelegt;Starterpaket erhalten;Erstellt;Letzte Bearbeitung
<?php
$line = 0;
@@ -97,12 +97,18 @@ while($data = mysqli_fetch_object($res)):
if($data->uid == "string") $data->uid = "";
$fcp = "";
if ($hausnummer->fcp_id) {
$fcp = ADBRimoFcp::get($hausnummer->fcp_id);
$fcp = $fcp->name;
}
?>
"<?=$campaign->name?>";"<?=$netzgebiet->extref?>";"<?=$netzgebiet->name?>";"<?=$data->extref?>";"<?=$data->ucode?>";"<?=implode(", ",$discounts)?>";"<?=$wohneinheit->oaid?>";"<?=($data->order_date) ? date("d.m.Y",$data->order_date) : ""?>";"<?=__($data->type,"preorder")?>";"<?=$status->code?>";"<?=$status->name?>";"<?=$wohneinheit->num ?>";<?=implode(";", $statusflags)?>;"<?=__($data->connection_type,"preorder")?>";"<?=$adrcd?>";"<?=$hausnummer->meridian?>";"<?=$hausnummer->rw?>";"<?=$hausnummer->hw?>";"<?=$strasse->name?>";"<?=$hausnummer->hausnummer?>";"<?=$plz->plz?>";"<?=$ortschaft->name?>";"<?=$unit_data?>";"<?=$hausnummer->gps_lat?>";"<?=$hausnummer->gps_long?>";<?=$data->connection_count?>;"<?=$data->company?>";"<?=$data->uid?>";"<?=$data->firstname?>";"<?=$data->lastname?>";"<?=$data->street?>";"<?=$data->zip?>";"<?=$data->city?>";"<?=$data->phone?>";"<?=$data->email?>";"<?=$partner->getCompanyOrName()?>";"<?=$data->ciftoken?>";"<?=$data->cifurl?>";"<?=$data->cifcableurl?>";<?=$addon_property?>;<?=$addon_inhouse?>;<?=($bep) ? "1" : "0"?>;<?=($inhouse) ? "1" : "0"?>;"<?=date("Y-m-d H:i:s",$data->create)?>";"<?=date("Y-m-d H:i:s",$data->edit)?>"
"<?=$campaign->name?>";"<?=$netzgebiet->extref?>";"<?=$netzgebiet->name?>";"<?=$data->extref?>";"<?=$data->ucode?>";"<?=implode(", ",$discounts)?>";"<?=$wohneinheit->oaid?>";"<?=$fcp?>";"<?=($data->order_date) ? date("d.m.Y",$data->order_date) : ""?>";"<?=__($data->type,"preorder")?>";"<?=$status->code?>";"<?=$status->name?>";"<?=count($hausnummer->wohneinheiten) ?>";<?=implode(";", $statusflags)?>;"<?=__($data->connection_type,"preorder")?>";"<?=$adrcd?>";"<?=$hausnummer->meridian?>";"<?=$hausnummer->rw?>";"<?=$hausnummer->hw?>";"<?=$strasse->name?>";"<?=$hausnummer->hausnummer?>";"<?=$plz->plz?>";"<?=$ortschaft->name?>";"<?=$unit_data?>";"<?=$hausnummer->gps_lat?>";"<?=$hausnummer->gps_long?>";<?=$data->connection_count?>;"<?=$data->company?>";"<?=$data->uid?>";"<?=$data->firstname?>";"<?=$data->lastname?>";"<?=$data->street?>";"<?=$data->zip?>";"<?=$data->city?>";"<?=$data->phone?>";"<?=$data->email?>";"<?=$partner->getCompanyOrName()?>";"<?=$data->ciftoken?>";"<?=$data->cifurl?>";"<?=$data->cifcableurl?>";<?=$addon_property?>;<?=$addon_inhouse?>;<?=($bep) ? "1" : "0"?>;<?=($inhouse) ? "1" : "0"?>;"<?=date("Y-m-d H:i:s",$data->create)?>";"<?=date("Y-m-d H:i:s",$data->edit)?>"
<?php
$line++;
if($line % 1000 === 0) {
flush();
}
endwhile;
endwhile;
@@ -565,8 +565,12 @@
</table>
<?php endforeach; ?>
<?php else: ?>
<?php elseif($preorder->status->code != "20"): ?>
<button type="button" class="btn btn-outline-primary create-workorder" onclick="createWorkorder(<?=$preorder->id?>)"><i class="fas fa-fw fa-plus"></i> <i class="fas fa-r"></i><i class="fas fa-fw fa-gears"></i> Wokorder erstellen</button>
<?php elseif($preorder->status->code == "20"): ?>
<div class="alert alert-info mt-2" role="alert">
<i class="fas fa-info-circle"></i> Diese Preorder ist auf Hold gesetzt. Es kann keine Workorder erstellt werden.
</div>
<?php endif; ?>
</div>
@@ -586,8 +590,10 @@
<td class="text-monospace"><?=$preorder->adb_wohneinheit->ftu_data["id"]?>
</tr>
</table>
</div>
<h3>FCP</h3>
<div class="col row">
<h3 >FCP</h3>
<?php
if($preorder->fcp): ?>
<table class="table table-sm table-striped">
@@ -610,9 +616,12 @@
</tr>
</table>
<?php else: ?>
<p>Kein FCP zugewiesen</p>
<div class="col-12 p-0">
<div class="alert alert-info mt-2" role="alert">
<i class="fas fa-info-circle"></i> Kein FCP zugewiesen/importert.
</div>
</div>
<?php endif; ?>
</div>
+4 -4
View File
@@ -103,12 +103,12 @@ include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php")
data-ucode="<?= $preorder->ucode ?>"
data-oaid="<?= $preorder->oaid ?>"
data-addr-name="<?= htmlspecialchars($preorder->company ?: "{$preorder->firstname} {$preorder->lastname}", ENT_QUOTES) ?>"
data-addr-street="<?= htmlspecialchars(trim("{$preorder->street} {$preorder->housenumber}"), ENT_QUOTES) ?>"
data-addr-zip="<?= $preorder->zip ?>"
data-addr-city="<?= htmlspecialchars($preorder->city, ENT_QUOTES) ?>"
data-addr-street="<?= htmlspecialchars($preorder->adb_hausnummer_id ? "{$preorder->adb_hausnummer->strasse->name} {$preorder->adb_hausnummer->hausnummer}" : trim("{$preorder->street} {$preorder->housenumber}"), ENT_QUOTES) ?>"
data-addr-zip="<?= htmlspecialchars($preorder->adb_hausnummer_id ? $preorder->adb_hausnummer->plz->plz : $preorder->zip, ENT_QUOTES) ?>"
data-addr-city="<?= htmlspecialchars($preorder->adb_hausnummer_id ? $preorder->adb_hausnummer->ortschaft->name : $preorder->city, ENT_QUOTES) ?>"
data-phone="<?= $preorder->phone ?>"
data-email="<?= $preorder->email ?>">
<td class="text-right align-middle">
<td class="text-right align-middle">
<button type="button" class="btn btn-sm btn-success font-weight-bold" onclick="printShippingSlip(<?= $preorder->id ?>)"><i class="fas fa-fw fa-print"></i> DRUCKEN</button>
</td>
<td class="text-center align-middle">
@@ -35,18 +35,27 @@
</div>
<div style="margin-top: 28pt">
<p>
<?php if($preorder->company): ?>
<?=nl2br($preorder->company)?><br />
<?php else: ?>
<br />
<?php endif; ?>
<?php if($preorder->lastname): ?>
<?=$preorder->firstname?> <?=$preorder->lastname?><br />
<?php endif; ?>
<?=$preorder->street?> <?=$preorder->housenumber?><br />
<?=$preorder->zip?> <?=$preorder->city?>
</p>
<p>
<?php if($preorder->company): ?>
<?=nl2br(htmlspecialchars($preorder->company))?><br />
<?php else: ?>
<br />
<?php endif; ?>
<?php if($preorder->lastname): ?>
<?=htmlspecialchars($preorder->firstname)?> <?=htmlspecialchars($preorder->lastname)?><br />
<?php endif; ?>
<?php if ($preorder->adb_hausnummer_id): ?>
<?= htmlspecialchars($preorder->adb_hausnummer->strasse->name) ?> <?= htmlspecialchars($preorder->adb_hausnummer->hausnummer) ?><br/>
<?php if ($preorder->adb_wohneinheit_id && (string)$preorder->adb_wohneinheit): ?>
<?= htmlspecialchars((string)$preorder->adb_wohneinheit) ?><br />
<?php endif; ?>
<?= htmlspecialchars($preorder->adb_hausnummer->plz->plz) ?> <?= htmlspecialchars($preorder->adb_hausnummer->ortschaft->name) ?>
<?php else: ?>
<?=htmlspecialchars($preorder->street)?> <?=htmlspecialchars($preorder->housenumber)?><br />
<?=htmlspecialchars($preorder->zip)?> <?=htmlspecialchars($preorder->city)?>
<?php endif; ?>
</p>
<p style="text-align: right; padding-top: 4pt;">Liezen, <?=date("d.m.Y")?></p>
<p style="padding-top: 4pt;">Liebe(r) <?=($preorder->firstname) ? $preorder->firstname : ""?> <?=($preorder->lastname) ? $preorder->lastname : ""?>,</p>
+13 -5
View File
@@ -22,12 +22,20 @@ for ($i = 1; $i <= 25; $i++) {
$time = $time - 604800;
}
$time = time();
$monthger = [
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember'
];
$month = [];
$date = new DateTime('first day of this month');
for ($i = 1; $i <= 12; $i++) {
$mon = date('n', $time);
$year = date('Y', $time);
$month[$time] = $monthger[$mon] . " " . $year;
$time = strtotime('-1 month', $time);
$mon = $date->format('n');
$year = $date->format('Y');
$month[$date->getTimestamp()] = $monthger[$mon] . " " . $year;
$date->modify('-1 month');
}
$years[time() + 31536000] = date('Y', time() + 31536000);
-620
View File
@@ -1,620 +0,0 @@
<?php
$siteTitle = "Benutzer";
?>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a>
</li>
<li class="breadcrumb-item"><a href="<?=self::getUrl("User")?>">Benutzer</a></li>
<li class="breadcrumb-item"><?=($action == "edit") ? "bearbeiten" : "neu"?></li>
</ol>
</div>
<h4 class="page-title">Benutzer</h4>
</div>
</div>
</div>
<!-- end page title -->
<form method="post" action="<?=$this->getUrl("User", "save")?>">
<!-- Main content -->
<div class="row">
<div class="col-lg">
<div class="card bg-light">
<div class="card-body">
<h4 class="header-title mb-3">Benutzer bearbeiten</h4>
<div class="card">
<div class="card-body">
<input type="hidden" name="id" value="<?=$user->id?>"/>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" class="form-control"
value="<?=$user->username?>"/>
</div>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" class="form-control"
value="<?=$user->name?>"/>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="text" id="email" name="email" class="form-control"
value="<?=$user->email?>"/>
</div>
<div class="form-group">
<label for="mobile">Handy Nr.:</label>
<input type="text" id="mobile" placeholder="+436641234xxx" name="mobile"
class="form-control" value="<?=$user->mobile?>"/>
</div>
<div class="form-group">
<label for="address_id">Firma/Person:</label>
<select name="address_id" id="address_id" class="form-control">
<option value=""></option>
<?php foreach($addresses as $address): ?>
<option value="<?=$address->id?>" <?=($address->id == $user->address_id || $address->id == $user->address_id) ? "selected='selected'" : ""?>><?=($address->company) ? $address->company : $address->getFullName()?><?=($address->customer_number) ? " (" . $address->customer_number . ")" : ""?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="admin">Admin:</label>
<select name="admin" id="admin"
class="form-control" <?=($user->id == 1) ? "disabled='disabled'" : ""?>>
<option value="false" <?=(isset($user) && !$user->isAdmin()) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->isAdmin() || $user->id == 1) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
<div class="form-group">
<label for="active">Aktiv:</label>
<select name="active" id="active" class="form-control">
<option value="false" <?=(isset($user) && !$user->active == 0) ? "selected='selected'" : ""?>>No</option>
<option value="true" <?=(isset($user) && $user->active == 1) ? "selected='selected'" : ""?>>Yes</option>
</select>
</div>
<div class="form-group">
<label for="technician">Techniker:</label>
<select name="technician" id="technician" class="form-control">
<option value="false" <?=(isset($user) && !$user->is("technician")) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->is("technician")) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
<div class="form-group">
<label for="employee"><?=TT_SYSOWNER_NAME_HTML?> Mitarbeiter:</label>
<select name="employee" id="employee" class="form-control">
<option value="false" <?=(isset($user) && !$user->is("employee")) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->is("employee")) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
<div id="employee-container" <?=(!isset($user) || !$user->is("employee")) ? "hidden" : ""?>>
<div class="form-group">
<label for="employee_number"><?=TT_SYSOWNER_NAME_HTML?> Mitarbeiternummer:</label>
<input type="text" id="employee_number" name="employee_number" class="form-control"
value="<?=(isset($user)) ? (new WorkerFlag($user->id, "employee_number"))->value() : ""?>" />
</div>
<div class="form-group">
<label for="employee_number">Vodia Outbound Identity - Domain:</label>
<input type="text" id="vodia_identity_domain" name="vodia_identity_domain" class="form-control"
value="<?=(isset($user)) ? (new WorkerFlag($user->id, "vodia_identity_domain"))->value() : ""?>" />
</div>
<div class="form-group">
<label for="employee_number">Vodia Outbound Identity - Username (Extension):</label>
<input type="text" id="vodia_identity_username" name="vodia_identity_username" class="form-control"
value="<?=(isset($user)) ? (new WorkerFlag($user->id, "vodia_identity_username"))->value() : ""?>" />
</div>
<div class="form-group">
<label for="employee_number">Vodia Outbound Identity - Standard Identität:</label>
<input type="text" id="vodia_identity_default" name="vodia_identity_default" class="form-control"
value="<?=(isset($user)) ? (new WorkerFlag($user->id, "vodia_identity_default"))->value() : ""?>" />
<small>+43 720 123456</small>
</div>
<div class="form-group">
<label for="project_api_key">OpenProject API Key:</label>
<input type="text" id="project_api_key" name="project_api_key" class="form-control"
value="<?=(isset($user)) ? (new WorkerFlag($user->id, "project_api_key"))->value() : ""?>" />
</div>
</div>
<hr />
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" class="form-control" value=""/>
</div>
<div class="form-group">
<label for="password2">Repeat Password:</label>
<input type="password" id="password2" name="password2" class="form-control"
value=""/>
</div>
<hr/>
<div class="form-group">
<label for="twofactorrequired">2FA erzwingen:</label>
<select name="twofactorrequired" id="twofactorrequired" class="form-control">
<option value="false" <?=(isset($user) && !$user->twofactorrequired) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=((!isset($user) || !$user->id) || (isset($user) && $user->twofactorrequired)) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title mb-3">Preorder</h4>
<div class="form-group" id="preorderfront-container">
<label for="preorderfront">Preorder Frontdesk (Semi-Readonly):</label>
<select name="preorderfront" id="preorderfront" class="form-control">
<option value="false" <?=(isset($user) && !$user->is("preorderfront")) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->is("preorderfront")) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
<div class="form-group" id="preorder-reporting-container">
<label for="preorderaddressreporting">Preorder Address Reporting API User:</label>
<select name="preorderaddressreporting" id="preorderaddressreporting"
class="form-control">
<option value="false" <?=(isset($user) && !$user->is("preorderaddressreporting")) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->is("preorderaddressreporting")) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
<small>z.B. Meridiam</small>
</div>
<div class="form-group" id="preorderlogistics-container">
<label for="preorderlogistics">Preorder Logistikpartner:</label>
<select name="preorderlogistics" id="preorderlogistics" class="form-control">
<option value="false" <?=(isset($user) && !$user->is("preorderlogistics")) ? "selected='selected'" : ""?>>
No
</option>
<option value="true" <?=(isset($user) && $user->is("preorderlogistics")) ? "selected='selected'" : ""?>>
Yes
</option>
</select>
</div>
<div class="form-group" id="preorder-network-container">
<label for="preorder_networks">Preorder Netzgebiete:</label>
<?php
$pns = [];
if($user->id) {
$pns = json_decode((new WorkerFlag($user->id, "preorder_networks"))->value());
if(!$pns) {
$pns = [];
}
}
?>
<select name="preorder_networks[]" id="preorder_networks" class="form-control"
multiple="multiple">
<?php foreach(NetworkModel::getAll() as $network): ?>
<option value="<?=$network->id?>" <?=(in_array($network->id, $pns)) ? "selected='selected'" : ""?>><?=$network->name?></option>
<?php endforeach; ?>
</select>
<small>Beschränkt Benutzer auf Netzgebiete. Überschreibt Netzgebiete der Firma. Wenn
leer werden Netzgebiete der Firma angezeigt</small>
</div>
<div class="form-group" id="preorderreadonly-container">
<label for="preorderreadonly">Preorder Readonly:</label>
<select name="preorderreadonly" id="preorderreadonly" class="form-control">
<option value="false" <?=(isset($user) && !$user->is("preorderreadonly")) ? "selected='selected'" : ""?>>
Read/Write
</option>
<option value="true" <?=(isset($user) && $user->is("preorderreadonly")) ? "selected='selected'" : ""?>>
Readonly
</option>
</select>
</div>
<h4 class="mt-2">Preorder Module</h4>
<div class="row mt-3">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Preorderpricing]"
id="can_preorderpricing"
value="1" <?=($user && $user->can("Preorderpricing")) ? "checked='checked'" : ""?> />
<label for="can_preorderpricing" class="form-check-label">Preorder
Bepreisung</label>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input"
name="can[PreorderpricingReadonly]" id="can_preorderpricingreadonly"
value="1" <?=$user && $user->can("PreorderpricingReadonly") ? "checked='checked'" : ""?> />
<label for="can_preorderpricingreadonly" class="form-check-label">Preorder
Bepreisung Readonly</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Preorderbilling]"
id="can_preorderbilling"
value="1" <?=($user && $user->can("Preorderbilling")) ? "checked='checked'" : ""?> />
<label for="can_preorderbilling" class="form-check-label">Preorder
Verrechnung</label>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input"
name="can[PreorderbillingReadonly]" id="can_preorderbillingreadonly"
value="1" <?=$user && $user->can("PreorderbillingReadonly") ? "checked='checked'" : ""?> />
<label for="can_preorderbillingreadonly" class="form-check-label">Preorder
Verrechnung Readonly</label>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title mb-3">Zustimmungserklärungen</h4>
<div class="form-group" id="constructionconsent-projects-container">
<label for="constructionconsent_projects">Zustimmungserklärungsprojekte:</label>
<?php
$constructionConsent_projects = [];
if($user->id) {
$constructionConsent_projects = json_decode((new WorkerFlag($user->id, "constructionConsent_projects"))->value());
if(!$constructionConsent_projects) {
$constructionConsent_projects = [];
}
}
?>
<select name="constructionconsent_projects[]" id="constructionconsent_projects"
class="form-control" multiple="multiple">
<?php foreach(ConstructionConsentProject::getAll() as $project): ?>
<option value="<?=$project->id?>" <?=(in_array($project->id, $constructionConsent_projects)) ? "selected='selected'" : ""?>><?=$project->name?></option>
<?php endforeach; ?>
</select>
<small>Benutzer kann nur Zustimmungserklärungen in diesen Projekten sehen</small>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title mb-3">Modulberechtigungen</h4>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Building]"
id="can_building"
value="1" <?=($user && $user->can("Building")) ? "checked='checked'" : ""?> />
<label for="can_building" class="form-check-label">Objekte & Anschlüsse
(Gebäude)</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Pipework]"
id="can_pipework"
value="1" <?=$user && $user->can("Pipework") ? "checked='checked'" : ""?> />
<label for="can_pipework" class="form-check-label">Tiefbau</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Linework]"
id="can_linework"
value="1" <?=$user && $user->can("Linework") ? "checked='checked'" : ""?> />
<label for="can_linework" class="form-check-label">Leitungsbau</label>
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Patching]"
id="can_patching"
value="1" <?=$user && $user->can("Patching") ? "checked='checked'" : ""?> />
<label for="can_patching" class="form-check-label">Patching</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Filestore]"
id="can_filestore"
value="1" <?=$user && $user->can("Filestore") ? "checked='checked'" : ""?> />
<label for="can_filestore" class="form-check-label">Filestore
(Netzbau)</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Cpeprovisioning]"
id="can_cpeprovisioning"
value="1" <?=$user && $user->can("Cpeprovisioning") ? "checked='checked'" : ""?> />
<label for="can_cpeprovisioning" class="form-check-label">CPE
Provisioning</label>
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Cpeshipping]"
id="can_cpeshipping"
value="1" <?=$user && $user->can("Cpeshipping") ? "checked='checked'" : ""?> />
<label for="can_cpeshipping" class="form-check-label">CPE Versand</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Voipnumbering]"
id="can_voipnumbering"
value="1" <?=$user && $user->can("Voipnumbering") ? "checked='checked'" : ""?> />
<label for="can_voipnumbering" class="form-check-label">VOIP
Nummernverwaltung</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Preorder]"
id="can_preorder"
value="1" <?=$user && $user->can("Preorder") ? "checked='checked'" : ""?> />
<label for="can_preorder" class="form-check-label">Vorbestellung</label>
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Order]"
id="can_order"
value="1" <?=$user && $user->can("Order") ? "checked='checked'" : ""?> />
<label for="can_order" class="form-check-label">Bestellung</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Billing]"
id="can_billing"
value="1" <?=$user && $user->can("Billing") ? "checked='checked'" : ""?> />
<label for="can_billing" class="form-check-label">Verrechnung</label>
</div>
</div>
</div>
<h4 class="card-title mb-3 mt-3">Lager</h4>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[WarehouseAdmin]"
id="can_warehouse_admin"
value="1" <?=($user && $user->can("WarehouseAdmin")) ? "checked='checked'" : ""?> />
<label for="can_warehouse_admin"
class="form-check-label">Lager-Admin</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[WarehouseUser]"
id="can_warehouse_user"
value="1" <?=($user && $user->can("WarehouseUser")) ? "checked='checked'" : ""?> />
<label for="can_warehouse_user" class="form-check-label">Lager-User</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[WarehouseEShop]"
id="can_warehouse_e_shop"
value="1" <?=($user && $user->can("WarehouseEShop")) ? "checked='checked'" : ""?> />
<label for="can_warehouse_e_shop" class="form-check-label">Energie
Steiermark Shop</label>
</div>
</div>
</div>
<h4 class="card-title mb-3 mt-3">Zusatzberechtigungen</h4>
<div class="row">
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Fibu]"
id="can_fibu"
value="1" <?=($user && $user->can("Fibu")) ? "checked='checked'" : ""?> />
<label for="can_fibu" class="form-check-label">Buchhaltung</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[Statistics]"
id="can_statistics"
value="1" <?=($user && $user->can("Statistics")) ? "checked='checked'" : ""?> />
<label for="can_statistics" class="form-check-label">Statistiken
anzeigen</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[ADBExtended]"
id="can_ADBExtended"
value="1" <?=($user && $user->can("ADBExtended")) ? "checked='checked'" : ""?> />
<label for="can_ADBExtended" class="form-check-label">Address-DB erweitert</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[AssetAdmin]"
id="can_AssetAdmin"
value="1" <?=($user && $user->can("AssetAdmin")) ? "checked='checked'" : ""?> />
<label for="can_AssetAdmin" class="form-check-label">Asset-Admin</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[RMLAdmin]"
id="can_RMLAdmin"
value="1" <?=($user && $user->can("RMLAdmin")) ? "checked='checked'" : ""?> />
<label for="can_RMLAdmin" class="form-check-label">RML-Workorder-Admin</label>
</div>
</div>
<div class="col-4">
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" name="can[RMLCompany]"
id="can_RMLCompany"
value="1" <?=($user && $user->can("RMLCompany")) ? "checked='checked'" : ""?> />
<label for="can_RMLCompany" class="form-check-label">RML-Workorder-Firma</label>
</div>
</div>
</div>
<hr/>
<div class="form-group">
<input type="submit" name="submit" value="Speichern" class="btn btn-primary"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<?php if($user->id): ?>
<div class="row">
<div class="col-lg">
<div class="card">
<div class="card-header">
<h3 class="card-title">API Key</h3>
</div>
<div class="card-body">
<div class="form-group">
<input type="text" class="form-control" value="<?=$user->apikey?>" disabled="disabled"/>
</div>
<div class="form-group">
<form method="post" action="<?=self::getUrl("User", "generateApikey")?>">
<input type="hidden" name="id" value="<?=$user->id?>"/>
<?php if($user->apikey): ?>
<button type="submit" class="btn btn-outline-primary"
onclick="if(!confirm('Achtung: Dadurch wird der bisherige API Key ungültig. Wirklich neuen API Key generieren?')) return false;">
Neuen API Key generieren
</button>
<?php else: ?>
<button type="submit" class="btn btn-outline-primary">API Key generieren</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</form>
<?php endif; ?>
<script type="text/javascript">
$(document).ready(function () {
$("#address_id").select2({
allowClear: true,
placeholder: ""
});
$("#preorder_networks").select2({
allowClear: true,
placeholder: "",
closeOnSelect: false
});
$("#constructionconsent_projects").select2({
allowClear: true,
placeholder: "",
closeOnSelect: false
});
<?php if(!$user || (!$user->is("preorderfront") && !$user->is("preorderaddressreporting")) ): ?>
//$("#preorder-network-container").hide();
<?php endif; ?>
<?php if($user && ($user->is("preorderfront")) ): ?>
//$("#preorder-reporting-container").hide();
<?php endif; ?>
<?php if($user && ($user->is("preorderaddressreporting")) ): ?>
//$("#preorderfront-container").hide();
<?php endif; ?>
$("select[name=preorderfront]").change(function () {
if ($("select[name=preorderfront]").val() == "true") {
$("#preorder-reporting-container").hide(500);
} else {
$("#preorder-reporting-container").show(500);
}
});
// preorder-reporting-container
$("select[name=preorderaddressreporting]").change(function () {
if ($("select[name=preorderaddressreporting]").val() == "true") {
$("#preorderfront-container").hide(400);
} else {
$("#preorderfront-container").show(400);
}
});
$("#employee").change(function () {
if ($("#employee").val() == "true") {
$("#employee-container").show(400);
} else {
$("#employee-container").hide(400);
}
});
});
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
@@ -0,0 +1,873 @@
<?php
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Workorders</title>
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<link rel="manifest" href="/js/pages/WorkorderBase/manifest.json">
<meta name="theme-color" content="#005384">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.30.1/locale/de.js"></script>
<script>
window.TT_CONFIG = <?= json_encode($JSGlobals ?? []) ?>;
moment.locale('de');
tailwind.config = {
darkMode: 'class', // Enable dark mode based on a class
theme: {
extend: {
colors: {
'primary': '#005384', // Dark Blue
'secondary': '#fac41b', // Yellow/Gold
},
}
}
}
</script>
<style>
html, body {
/* Prevents the rubber-band scroll effect on iOS and pull-to-refresh on Android */
overscroll-behavior: none;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
main {
/* Prevents scrolling within the main container from affecting the body */
overscroll-behavior-y: contain;
}
.slide-enter-active, .slide-leave-active { transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.slide-enter-from, .slide-leave-to { transform: translateX(100%); }
.list-container.panel-open {
transform: scale(0.95);
filter: blur(4px);
opacity: 0.7;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), filter 0.35s, opacity 0.35s;
}
.list-container {
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), filter 0.35s, opacity 0.35s;
}
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0,0,0,0.4);
transition: opacity 0.35s ease;
z-index: 15;
}
.overlay-enter-from, .overlay-leave-to { opacity: 0; }
.overlay-enter-to, .overlay-leave-from { opacity: 1; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease-in-out; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin { animation: spin 1.5s ease-in-out infinite; }
</style>
</head>
<body class="transition-colors duration-300 overflow-hidden">
<div id="app" class="h-screen w-screen overflow-hidden antialiased"></div>
<script>
const { createApp, ref, reactive, computed, onMounted, watch, nextTick } = Vue;
const app = createApp({
setup() {
// --- STATE ---
const workorders = ref([]);
const selectedWorkorder = ref(null);
const isLoading = ref(true);
const isDetailsLoading = ref(false);
const isDetailsPanelOpen = ref(false);
const searchTerm = ref('');
const documentation = reactive({ docs: [], journals: [] });
const tenantConfig = ref(null);
const tempAdditionalInfo = ref('');
const isEditingInfo = ref(false);
const newJournalEntry = ref('');
const isUploading = ref(false);
const uploadModal = reactive({ show: false, files: null, documentType: '' });
const problemModal = reactive({ show: false, selectedInterventions: [], details: {} });
const fullscreenViewer = reactive({ show: false, item: null });
const missingTasksPopover = reactive({ show: false, tasks: [] });
const installModal = reactive({ show: false });
const isStandalone = ref(false);
const selectedFcp = ref('all');
const isFcpSelectOpen = ref(false);
const fcpSearchTerm = ref('');
const fcpInputRef = ref(null); // For autofocusing FCP search
const isSettingsOpen = ref(false);
const theme = ref('system'); // 'light', 'dark', 'system'
const showThemePicker = ref(false);
const API_BASE_URL = window.TT_CONFIG.BASE_PATH || '/WorkorderCompany';
const api = axios.create({ baseURL: API_BASE_URL });
// --- COMPUTED ---
const fcpOptions = computed(() => {
if (!workorders.value || workorders.value.length === 0) {
return [{ value: 'all', text: 'Alle FCPs' }];
}
const fcps = [...new Set(workorders.value.map(wo => wo.rimo_fcp_name).filter(Boolean))].sort();
const options = fcps.map(fcp => ({ value: fcp, text: fcp }));
return [{ value: 'all', text: 'Alle FCPs' }, ...options];
});
const filteredFcpOptions = computed(() => {
if (!fcpSearchTerm.value) {
return fcpOptions.value;
}
const lowerCaseSearch = fcpSearchTerm.value.toLowerCase();
return fcpOptions.value.filter(option =>
option.text.toLowerCase().includes(lowerCaseSearch)
);
});
const selectedFcpText = computed(() => {
const selectedOption = fcpOptions.value.find(opt => opt.value === selectedFcp.value);
return selectedOption ? selectedOption.text : 'Alle FCPs';
});
const filteredWorkorders = computed(() => {
let filtered = workorders.value;
if (selectedFcp.value !== 'all') {
filtered = filtered.filter(wo => wo.rimo_fcp_name === selectedFcp.value);
}
if (searchTerm.value.length > 2) {
const lowerSearch = searchTerm.value.toLowerCase();
filtered = filtered.filter(wo =>
wo.id.toString().includes(lowerSearch) ||
(wo.customerName && wo.customerName.toLowerCase().includes(lowerSearch)) ||
(wo.street && wo.street.toLowerCase().includes(lowerSearch)) ||
(wo.city && wo.city.toLowerCase().includes(lowerSearch)) ||
(wo.oaid && wo.oaid.toLowerCase().includes(lowerSearch)) ||
(wo.rimo_fcp_name && wo.rimo_fcp_name.toLowerCase().includes(lowerSearch))
);
}
const getStatusRank = (status) => {
switch (status) {
case 'scheduled':
case 'civil_engineering_completed': return 0;
case 'assigned':
case 'new':
case 'problem_solved': return 1;
case 'intervention_required':
case 'correction_requested':
case 'civil_engineering_required': return 2;
case 'documented':
case 'completed': return 3;
case 'cancelled': return 4;
default: return 99;
}
};
return filtered.sort((a, b) => {
const rankA = getStatusRank(a.status);
const rankB = getStatusRank(b.status);
if (rankA !== rankB) return rankA - rankB;
if (rankA === 0) {
const dateA = a.appointmentDate || Infinity;
const dateB = b.appointmentDate || Infinity;
if (dateA === dateB) return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
return dateA - dateB;
}
return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
});
});
const googleMapsLink = computed(() => {
if (!selectedWorkorder.value) return '#';
const { street, hausnummer, plz, city } = selectedWorkorder.value;
const address = encodeURIComponent(`${street} ${hausnummer}, ${plz} ${city}`);
return `https://maps.google.com/maps?q=${address}`;
});
const checklist = computed(() => {
if (!tenantConfig.value?.documentationTypes || !Array.isArray(tenantConfig.value.documentationTypes)) return [];
return tenantConfig.value.documentationTypes.map(reqType => {
const isCompleted = documentation.docs.some(doc => doc.documentType === reqType.value);
return { ...reqType, completed: isCompleted };
});
});
const isChecklistComplete = computed(() => {
if (checklist.value.length === 0) return true;
return checklist.value.every(item => item.completed);
});
const translatedDocs = computed(() => {
if (!documentation.docs.length || !tenantConfig.value?.documentationTypes) {
return documentation.docs;
}
const typeMap = new Map(tenantConfig.value.documentationTypes.map(t => [t.value, t.text]));
return documentation.docs.map(doc => ({
...doc,
translatedName: typeMap.get(doc.documentType) || doc.documentType,
}));
});
const filteredJournals = computed(() => {
return documentation.journals.filter(j => !j.text.toLowerCase().includes('wurde zugewiesen.'));
});
// --- METHODS ---
const applyTheme = () => {
const isDark = localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? '#0f172a' : '#005384');
}
};
const setTheme = (newTheme) => {
if (!['light', 'dark', 'system'].includes(newTheme)) return;
theme.value = newTheme;
if (newTheme === 'system') {
localStorage.removeItem('theme');
} else {
localStorage.setItem('theme', newTheme);
}
applyTheme();
isSettingsOpen.value = false;
if (showThemePicker.value) showThemePicker.value = false;
};
const getStatusInfo = (status) => {
const statuses = {
'new': { text: 'Neu', color: 'bg-blue-500' }, 'assigned': { text: 'Zugewiesen', color: 'bg-sky-500' },
'scheduled': { text: 'Geplant', color: 'bg-amber-500' }, 'correction_requested': { text: 'Korrektur', color: 'bg-red-500' },
'intervention_required': { text: 'Eingriff', color: 'bg-red-700' }, 'civil_engineering_required': { text: 'Tiefbau', color: 'bg-orange-500' },
'civil_engineering_completed': { text: 'Tiefbau OK', color: 'bg-green-500' }, 'problem_solved': { text: 'Problem gelöst', color: 'bg-teal-500' },
'documented': { text: 'Dokumentiert', color: 'bg-indigo-500' }, 'completed': { text: 'Abgeschlossen', color: 'bg-slate-500' },
'cancelled': { text: 'Storniert', color: 'bg-gray-600' }, 'default': { text: 'Unbekannt', color: 'bg-gray-400' }
};
return statuses[status] || statuses.default;
};
const formatDate = (timestamp, format = 'DD.MM.YYYY') => {
if (!timestamp) return '';
return moment.unix(timestamp).format(format);
};
const fetchWorkorders = async () => {
isLoading.value = true;
try {
const response = await api.post(`/get`, { pagination: { page: 1, per_page: 500 } });
workorders.value = response.data.rows;
} catch (error) { console.error("Failed to fetch workorders:", error); }
finally { isLoading.value = false; }
};
const fetchDetails = async (workorderId) => {
isDetailsLoading.value = true;
try {
const [docRes, configRes] = await Promise.all([
api.get(`/getDocumentation?workorderId=${workorderId}`),
api.get(`/getTenantConfig?workorderId=${workorderId}`)
]);
documentation.docs = docRes.data.docs.map(d => ({...d, isPdf: d.mimetype === 'application/pdf'}));
documentation.journals = docRes.data.journals;
if (configRes.data.success) {
tenantConfig.value = configRes.data;
}
} catch (e) { console.error("Could not load details", e); }
finally { isDetailsLoading.value = false; }
};
const openDetails = (workorder) => {
selectedWorkorder.value = workorder;
isDetailsPanelOpen.value = true;
fetchDetails(workorder.id);
};
const closeDetails = () => {
isDetailsPanelOpen.value = false;
setTimeout(() => {
selectedWorkorder.value = null;
documentation.docs = []; documentation.journals = [];
tenantConfig.value = null; isEditingInfo.value = false;
}, 350);
};
const startEditInfo = () => {
tempAdditionalInfo.value = selectedWorkorder.value.additionalInfo || '';
isEditingInfo.value = true;
};
const saveAdditionalInfo = async () => {
const newInfo = tempAdditionalInfo.value;
try {
await api.post('/updateAdditionalInfo', { workorderId: selectedWorkorder.value.id, additionalInfo: newInfo });
selectedWorkorder.value.additionalInfo = newInfo;
const woInList = workorders.value.find(w => w.id === selectedWorkorder.value.id);
if(woInList) woInList.additionalInfo = newInfo;
await fetchDetails(selectedWorkorder.value.id); // to refresh journal
} catch(e) { console.error("Failed to save info", e); }
finally { isEditingInfo.value = false; }
};
const addJournalEntry = async () => {
if (!newJournalEntry.value.trim()) return;
try {
const response = await api.post('/addJournal', { workorderId: selectedWorkorder.value.id, text: newJournalEntry.value });
documentation.journals = response.data.journals;
newJournalEntry.value = '';
await nextTick(() => {
const journalContainer = document.querySelector('.journal-container');
if(journalContainer) journalContainer.scrollTop = journalContainer.scrollHeight;
});
} catch(e) { console.error("Failed to add journal entry", e); }
};
const handleFileSelect = (event) => {
if (!event.target.files.length) return;
uploadModal.files = event.target.files;
uploadModal.documentType = tenantConfig.value?.documentationTypes?.[0]?.value || 'general';
uploadModal.show = true;
};
const executeUpload = async () => {
if (!uploadModal.files) return;
isUploading.value = true;
const formData = new FormData();
formData.append('workorderId', selectedWorkorder.value.id);
formData.append('documentType', uploadModal.documentType);
for (let i = 0; i < uploadModal.files.length; i++) {
formData.append('files[]', uploadModal.files[i]);
}
try {
await api.post(`/uploadDocumentation`, formData, { headers: { 'Content-Type': 'multipart/form-data' } });
await fetchDetails(selectedWorkorder.value.id);
} catch (error) { console.error('Upload failed:', error); }
finally {
isUploading.value = false;
uploadModal.show = false;
uploadModal.files = null;
}
};
const submitProblem = async () => {
if (problemModal.selectedInterventions.length === 0) return;
let journalParts = [];
const sortedInterventions = [...problemModal.selectedInterventions].sort((a, b) => a.value.localeCompare(b.value));
for (const type of sortedInterventions) {
let text = type.text;
const needsDetail = type.text.includes('X') || type.text.toLowerCase().includes('sonstiges');
if (needsDetail) {
const detail = problemModal.details[type.value] || '';
if (!detail) {
alert(`Bitte geben Sie Details für "${type.text}" an.`);
return;
}
text = text.includes('X') ? text.replace('X', detail) : `${text}: ${detail}`;
}
journalParts.push(text);
}
const combinedText = journalParts.join('\n');
try {
await api.post('/requestIntervention', {
workorderId: selectedWorkorder.value.id,
journalText: combinedText
});
await fetchWorkorders();
closeDetails();
} catch(e) { console.error("Failed to report problem", e); }
finally { problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}; }
};
const handleCompleteClick = () => {
if (isChecklistComplete.value) {
if (confirm("Möchten Sie diesen Auftrag wirklich abschließen?")) {
completeWorkorder();
}
} else {
missingTasksPopover.tasks = checklist.value.filter(t => !t.completed).map(t => t.text);
missingTasksPopover.show = true;
setTimeout(() => missingTasksPopover.show = false, 4000);
}
};
const completeWorkorder = async () => {
try {
await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id });
await fetchWorkorders();
closeDetails();
} catch(e) { console.error("Failed to complete workorder", e); }
};
const selectFcp = (fcpValue) => {
selectedFcp.value = fcpValue;
isFcpSelectOpen.value = false;
};
onMounted(() => {
fetchWorkorders();
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches;
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
theme.value = savedTheme;
} else {
showThemePicker.value = true;
}
applyTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
});
watch(isFcpSelectOpen, (isOpen) => {
if (isOpen) {
nextTick(() => {
fcpInputRef.value?.focus();
});
} else {
fcpSearchTerm.value = '';
}
});
return {
isLoading, isDetailsLoading, filteredWorkorders, searchTerm, isDetailsPanelOpen, selectedWorkorder, documentation, tenantConfig,
tempAdditionalInfo, isEditingInfo, newJournalEntry, uploadModal, problemModal, isUploading, isChecklistComplete,
checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals, installModal, isStandalone,
selectedFcp, isFcpSelectOpen, fcpOptions, selectedFcpText, fcpSearchTerm, filteredFcpOptions, fcpInputRef,
isSettingsOpen, theme, showThemePicker,
fetchWorkorders, openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo,
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme
};
},
template: `
<div class="relative h-full w-full">
<transition name="overlay">
<div v-if="isDetailsPanelOpen || installModal.show || isFcpSelectOpen || isSettingsOpen" @click="closeDetails(); isFcpSelectOpen = false; isSettingsOpen = false;" class="overlay"></div>
</transition>
<div :class="{'panel-open': isDetailsPanelOpen}" class="list-container flex flex-col h-full bg-slate-100 dark:bg-slate-900 overflow-hidden transition-colors duration-300">
<header class="bg-white dark:bg-slate-800 shadow dark:shadow-md p-4 flex-shrink-0 z-10">
<div class="grid grid-cols-3 items-center">
<div class="justify-self-start">
<button @click="fetchWorkorders" class="p-2 rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 active:bg-slate-200 dark:active:bg-slate-600 focus:outline-none focus:ring-0">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
</button>
</div>
<div class="justify-self-center">
<img src="/assets/images/xinon-full-transparent.png" alt="Logo" class="h-8 w-auto block dark:hidden">
<img src="/assets/images/xinon-full-transparent-white.png" alt="Logo" class="h-8 w-auto hidden dark:block">
</div>
<div class="justify-self-end">
<button @click="isSettingsOpen = true" class="p-2 rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 active:bg-slate-200 dark:active:bg-slate-600 focus:outline-none focus:ring-0">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5-38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
</button>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
<input type="text" v-model="searchTerm" placeholder="Suche..." inputmode="search" class="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:placeholder-slate-300">
<button @click="isFcpSelectOpen = true" class="w-full p-3 border border-slate-300 rounded-lg bg-white dark:bg-slate-700 dark:border-slate-600 text-left flex justify-between items-center text-sm">
<span class="truncate pr-2 text-slate-800 dark:text-slate-100">{{ selectedFcpText }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400 dark:text-slate-400 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</header>
<main class="flex-grow overflow-y-auto p-2 pb-16">
<div v-if="isLoading" class="space-y-3 p-2 animate-pulse">
<div v-for="i in 4" :key="i" class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-md">
<div class="flex justify-between items-start">
<div class="flex-grow pr-2 min-w-0">
<div class="h-5 bg-slate-200 dark:bg-slate-700 rounded w-3/4 mb-1.5"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-full mb-2"></div>
<div class="space-y-1.5">
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded w-1/2"></div>
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded w-2/3"></div>
</div>
</div>
<div class="flex-shrink-0 ml-2 text-right space-y-1">
<div class="h-5 w-24 bg-slate-200 dark:bg-slate-700 rounded-full ml-auto"></div>
<div class="h-4 w-28 bg-slate-200 dark:bg-slate-700 rounded ml-auto"></div>
<div class="h-3 w-20 bg-slate-200 dark:bg-slate-700 rounded ml-auto"></div>
</div>
</div>
</div>
</div>
<div v-else-if="filteredWorkorders.length === 0" class="text-center p-10"><p class="text-slate-500 dark:text-slate-300">Keine Aufträge gefunden.</p></div>
<div v-else class="space-y-3">
<div v-for="wo in filteredWorkorders" :key="wo.id" @click="openDetails(wo)" class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-md dark:shadow-lg cursor-pointer transition active:scale-[0.98]">
<div class="flex justify-between items-start">
<div class="flex-grow pr-2 min-w-0">
<p class="font-bold text-slate-800 dark:text-slate-50 break-words"><span class="dark:text-secondary">#{{ wo.id }}</span> | {{ wo.customerName || 'N/A' }}</p>
<p class="text-sm text-slate-500 dark:text-slate-300 break-words">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
<div class="items-center text-xs text-slate-400 dark:text-slate-400 mt-1">
<span class="mr-2">OAID: {{ wo.oaid || 'N/A' }}</span><br>
<span class="truncate">FCP: {{ wo.rimo_fcp_name || 'N/A' }}</span>
</div>
</div>
<div class="flex-shrink-0 ml-2 text-right space-y-1">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium text-white" :class="getStatusInfo(wo.status).color">{{ getStatusInfo(wo.status).text }}</span>
<p class="text-sm font-semibold text-slate-600 dark:text-slate-200">{{ formatDate(wo.appointmentDate, 'DD.MM HH:mm') }}</p>
<p class="text-xs text-red-500">Frist: {{ formatDate(wo.deadlineDate) }}</p>
</div>
</div>
</div>
</div>
</main>
</div>
<transition name="slide">
<div v-if="isDetailsPanelOpen && selectedWorkorder" class="fixed inset-0 bg-slate-50 dark:bg-slate-950 z-20 flex flex-col shadow-2xl">
<header class="bg-white dark:bg-slate-900 p-4 flex justify-between items-center border-b border-slate-200 dark:border-slate-800 flex-shrink-0">
<div class="flex items-center min-w-0">
<div class="h-6 w-auto mr-4">
<img src="/assets/images/xinon-full-transparent.png" alt="Logo" class="h-6 w-auto block dark:hidden">
<img src="/assets/images/xinon-full-transparent-white.png" alt="Logo" class="h-6 w-auto hidden dark:block">
</div>
<h2 class="text-xl font-bold text-primary dark:text-secondary truncate">Auftrag #{{ selectedWorkorder.id }}</h2>
</div>
<button @click="closeDetails" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-600 dark:text-slate-200" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
</header>
<div class="overflow-y-auto p-4 flex-grow space-y-4">
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800 space-y-3 text-sm">
<div class="flex items-center text-base font-bold text-slate-800 dark:text-slate-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-500 dark:text-slate-300" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" /></svg>
<span>{{ selectedWorkorder.customerCompany || selectedWorkorder.customerName }}</span>
</div>
<a :href="googleMapsLink" target="_blank" class="flex items-center text-primary dark:text-secondary hover:underline">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" /></svg>
<span>{{ selectedWorkorder.street }} {{ selectedWorkorder.hausnummer }}, {{ selectedWorkorder.plz }} {{ selectedWorkorder.city }}</span>
</a>
<div class="border-t border-slate-200 dark:border-slate-800 pt-3 mt-3 grid grid-cols-2 gap-2 text-sm">
<div>
<p class="text-xs text-slate-500 dark:text-slate-300 font-semibold">OAID</p>
<p class="text-slate-800 dark:text-slate-100">{{ selectedWorkorder.oaid || 'N/A' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 dark:text-slate-300 font-semibold">FCP</p>
<p class="text-slate-800 dark:text-slate-100">{{ selectedWorkorder.rimo_fcp_name || 'N/A' }}</p>
</div>
</div>
<div class="border-t border-slate-200 dark:border-slate-800 pt-3 space-y-2">
<a :href="'mailto:' + selectedWorkorder.email" class="flex items-center text-primary dark:text-secondary hover:underline"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /></svg><span>{{ selectedWorkorder.email }}</span></a>
<a :href="'tel:' + selectedWorkorder.phone" class="flex items-center text-primary dark:text-secondary hover:underline"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" /></svg><span>{{ selectedWorkorder.phone }}</span></a>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-slate-700 dark:text-secondary">Notiz</h3>
<button v-if="!isEditingInfo" @click="startEditInfo" class="flex items-center text-sm font-medium text-primary dark:text-primary bg-slate-100 hover:bg-slate-200 dark:bg-secondary dark:hover:bg-yellow-400 px-3 py-1.5 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg> Bearbeiten
</button>
</div>
<div v-if="isEditingInfo">
<textarea v-model="tempAdditionalInfo" class="w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" rows="4"></textarea>
<div class="flex justify-end space-x-2 mt-2">
<button @click="isEditingInfo = false" class="px-3 py-1.5 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="saveAdditionalInfo" class="px-3 py-1.5 bg-secondary text-primary font-bold rounded-md text-sm">Speichern</button>
</div>
</div>
<p v-else class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-200">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-3">Checkliste</h3>
<div v-if="isDetailsLoading" class="space-y-3 animate-pulse">
<div v-for="i in 4" :key="i" class="flex items-center">
<div class="h-5 w-5 rounded-full bg-slate-200 dark:bg-slate-700 mr-2"></div>
<div class="h-4 w-3/4 rounded bg-slate-200 dark:bg-slate-700"></div>
</div>
</div>
<div v-else>
<ul v-if="checklist.length > 0" class="space-y-2">
<li v-for="item in checklist" :key="item.value" class="flex items-center text-sm">
<svg v-if="item.completed" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-green-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-400 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" /></svg>
<span :class="{'text-slate-500 dark:text-slate-300 line-through': item.completed, 'text-slate-800 dark:text-slate-100': !item.completed}">{{ item.text }}</span>
</li>
</ul>
<p v-else class="text-sm text-slate-500 dark:text-slate-300">Keine Checklisten-Einträge vorhanden.</p>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-2">Dokumentation</h3>
<label for="file-upload" class="w-full inline-flex items-center justify-center px-4 py-2 border border-dashed border-slate-300 dark:border-slate-700 text-sm font-medium rounded-md text-slate-700 dark:text-slate-200 bg-slate-50 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
<span>Foto/Dokument hinzufügen</span>
</label>
<input id="file-upload" type="file" class="hidden" @change="handleFileSelect" multiple accept="image/*,application/pdf">
<div v-if="translatedDocs.length > 0" class="grid grid-cols-3 sm:grid-cols-4 gap-2 mt-4">
<div v-for="doc in translatedDocs" :key="doc.id" @click="fullscreenViewer.show = true; fullscreenViewer.item = doc" class="relative aspect-square bg-slate-100 dark:bg-slate-800 rounded-md overflow-hidden cursor-pointer group">
<template v-if="doc.isPdf">
<div class="h-full w-full flex items-center justify-center bg-red-50 dark:bg-red-900/20 p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-red-500 dark:text-red-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H4zm3 4a1 1 0 000 2h6a1 1 0 100-2H7zm0 4a1 1 0 100 2h6a1 1 0 100-2H7zm0 4a1 1 0 100 2h4a1 1 0 100-2H7z" clip-rule="evenodd" /></svg>
</div>
</template>
<template v-else>
<img :src="'/File/show?id=' + doc.fileId + '&size=small'" class="h-full w-full object-cover">
</template>
<div class="absolute inset-x-0 bottom-0 p-1 bg-black bg-opacity-50">
<p class="text-white text-xs truncate">{{ doc.translatedName }}</p>
</div>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg></div>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-4">Journal</h3>
<div v-if="isDetailsLoading" class="animate-pulse">
<div class="flex items-start">
<div class="flex-shrink-0 bg-slate-200 dark:bg-slate-700 h-8 w-8 rounded-full mr-3"></div>
<div class="flex-grow space-y-2">
<div class="h-4 w-full rounded bg-slate-200 dark:bg-slate-700"></div>
<div class="h-3 w-1/2 rounded bg-slate-200 dark:bg-slate-700"></div>
</div>
</div>
</div>
<div v-else class="space-y-4 max-h-60 overflow-y-auto pr-2 journal-container">
<div v-if="filteredJournals.length === 0"><p class="text-sm text-slate-500 dark:text-slate-300">Keine Einträge.</p></div>
<div v-for="entry in filteredJournals" :key="entry.id" class="flex items-start">
<div class="flex-shrink-0 bg-secondary h-8 w-8 rounded-full flex items-center justify-center mr-3"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg></div>
<div class="flex-grow">
<p class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-100">{{ entry.text }}</p>
<p class="text-xs text-slate-400 dark:text-slate-400 mt-1">{{ entry.createByName }} - {{ formatDate(entry.create, 'DD.MM.YY HH:mm') }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-800">
<textarea v-model="newJournalEntry" placeholder="Neuer Eintrag..." class="w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" rows="3"></textarea>
<button @click="addJournalEntry" class="mt-2 w-full px-4 py-2 bg-secondary text-primary font-bold rounded-md text-sm">Senden</button>
</div>
</div>
</div>
<footer class="bg-white dark:bg-slate-900 p-2 border-t border-slate-200 dark:border-slate-800 flex-shrink-0 grid grid-cols-2 gap-2 pt-2 px-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
<button @click="problemModal.show = true" class="w-full px-4 py-3 bg-red-600 text-white font-bold rounded-md text-center">Problem melden</button>
<div class="relative">
<button @click="handleCompleteClick" class="w-full px-4 py-3 bg-green-600 text-white font-bold rounded-md text-center disabled:bg-slate-300">Abschließen</button>
<transition name="fade">
<div v-if="missingTasksPopover.show" class="absolute bottom-full right-0 mb-2 w-72 bg-red-700 text-white text-sm rounded-lg shadow-lg p-3">
<h4 class="font-bold mb-1">Fehlende Checklisten-Punkte:</h4>
<ul class="list-disc list-inside space-y-1">
<li v-for="task in missingTasksPopover.tasks" :key="task">{{ task }}</li>
</ul>
<div class="absolute bottom-[-5px] right-[calc(6rem-8px)] w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-red-700"></div>
</div>
</transition>
</div>
</footer>
</div>
</transition>
<transition name="fade">
<div v-if="isFcpSelectOpen" class="fixed inset-0 z-30 flex items-start justify-center p-4 pt-20" @click.self="isFcpSelectOpen = false">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100">
<div class="flex justify-between items-center mb-2 flex-shrink-0">
<h3 class="font-bold text-lg">FCP auswählen</h3>
<button @click="isFcpSelectOpen = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<div class="relative mb-2 flex-shrink-0">
<input type="text" v-model="fcpSearchTerm" ref="fcpInputRef" inputmode="search" placeholder="FCP suchen..." class="w-full p-2 pl-8 border border-slate-300 rounded-md dark:bg-slate-700 dark:border-slate-600">
<svg class="absolute left-2 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
<ul class="flex-grow overflow-y-auto -mr-2 pr-2">
<li v-for="option in filteredFcpOptions" :key="option.value" @click="selectFcp(option.value)"
class="flex justify-between items-center p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer text-sm font-medium"
:class="{'bg-primary/10 text-primary dark:bg-secondary/20 dark:text-secondary': selectedFcp === option.value}">
<span>{{ option.text }}</span>
<svg v-if="selectedFcp === option.value" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary dark:text-secondary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</li>
<li v-if="filteredFcpOptions.length === 0" class="text-sm text-slate-500 dark:text-slate-300 p-3">Kein FCP gefunden.</li>
</ul>
</div>
</div>
</transition>
<div v-if="uploadModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-30 flex items-start justify-center p-4 pt-20" @click.self="uploadModal.show = false">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100" @click.stop>
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<h3 class="font-bold text-lg">Dokumenttyp wählen</h3>
<button @click="uploadModal.show = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<ul class="flex-grow overflow-y-auto -mr-2 pr-2 space-y-1 mb-4">
<li v-for="type in tenantConfig.documentationTypes" :key="type.value" @click="uploadModal.documentType = type.value"
class="flex justify-between items-center p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer text-sm font-medium"
:class="{'bg-secondary/20 text-secondary': uploadModal.documentType === type.value}">
<span>{{ type.text }}</span>
<svg v-if="uploadModal.documentType === type.value" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-secondary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</li>
<li v-if="!tenantConfig.documentationTypes || tenantConfig.documentationTypes.length === 0">
<p class="text-sm text-slate-500 dark:text-slate-300 p-3">Keine Dokumenttypen konfiguriert.</p>
</li>
</ul>
<div class="flex justify-end space-x-2 mt-auto flex-shrink-0 border-t border-slate-200 dark:border-slate-700 pt-3">
<button @click="uploadModal.show = false" class="px-4 py-2 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="executeUpload" :disabled="isUploading" class="px-4 py-2 bg-primary text-white rounded-md disabled:bg-slate-400 text-sm font-medium">{{ isUploading ? 'Lade...' : 'Hochladen' }}</button>
</div>
</div>
</div>
<div v-if="problemModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-30 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100">
<h3 class="font-bold text-lg mb-4 flex-shrink-0">Problem melden</h3>
<div class="flex-grow overflow-y-auto pr-2 space-y-2 mb-4">
<div v-for="type in tenantConfig.interventionTypes" :key="type.value">
<label class="flex items-center p-3 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition cursor-pointer">
<input type="checkbox" :value="type" v-model="problemModal.selectedInterventions" class="h-5 w-5 rounded text-primary focus:ring-primary focus:ring-2 focus:ring-offset-1">
<span class="ml-3 text-sm font-medium">{{ type.text.replace('X', '...') }}</span>
</label>
<input v-if="problemModal.selectedInterventions.some(i => i.value === type.value) && (type.text.includes('X') || type.text.toLowerCase().includes('sonstiges'))"
v-model="problemModal.details[type.value]"
type="text" class="w-full p-2 border rounded-md mt-1 text-sm focus:ring-primary focus:border-primary dark:bg-slate-700 dark:border-slate-600" placeholder="Details hier eingeben...">
</div>
</div>
<div class="flex justify-end space-x-2 mt-auto flex-shrink-0">
<button @click="problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}" class="px-4 py-2 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md">Abbrechen</button>
<button @click="submitProblem" class="px-4 py-2 bg-red-600 text-white rounded-md">Senden</button>
</div>
</div>
</div>
<transition name="fade">
<div v-if="isSettingsOpen" class="fixed inset-0 z-30 flex items-start justify-center p-4 pt-20" @click.self="isSettingsOpen = false">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-sm flex flex-col text-slate-800 dark:text-slate-100">
<div class="p-4 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center">
<h3 class="font-bold text-lg">Einstellungen</h3>
<button @click="isSettingsOpen = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<div class="p-4 space-y-4">
<div>
<h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-300">Farbschema</h4>
<div class="grid grid-cols-3 gap-2">
<button @click="setTheme('light')" :class="{'bg-primary text-white': theme === 'light'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">Hell</button>
<button @click="setTheme('dark')" :class="{'bg-primary text-white': theme === 'dark'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">Dunkel</button>
<button @click="setTheme('system')" :class="{'bg-primary text-white': theme === 'system'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">System</button>
</div>
</div>
<div v-if="!isStandalone">
<h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-300">App</h4>
<button @click="installModal.show = true; isSettingsOpen = false" class="w-full text-left p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 text-sm font-medium">App installieren</button>
</div>
</div>
<div class="p-4 border-t border-slate-200 dark:border-slate-700">
<a href="https://thetool.xinon.at/WorkorderCompany/logout" class="w-full text-left p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 text-sm font-medium flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clip-rule="evenodd" /></svg>
Logout
</a>
</div>
<footer class="p-4 mt-2 text-center text-xs text-slate-500 dark:text-slate-300 space-y-2">
<img src="/assets/images/xinon-sm.png" class="h-10 mx-auto" alt="XINON Logo">
<p>
powered by XINON GmbH<br>
<a href="https://xinon.at/impressum/" target="_blank" class="hover:underline">Impressum</a>
</p>
</footer>
</div>
</div>
</transition>
<transition name="fade">
<div v-if="showThemePicker" class="fixed inset-0 bg-black bg-opacity-60 z-40 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-xs text-center shadow-2xl">
<h3 class="font-bold text-lg mb-2 dark:text-white">Willkommen!</h3>
<p class="text-sm text-slate-600 dark:text-slate-200 mb-6">Wähle dein bevorzugtes Farbschema.</p>
<div class="flex flex-col space-y-3">
<button @click="setTheme('light')" class="w-full px-4 py-3 bg-slate-200 text-slate-800 font-bold rounded-md">Hell</button>
<button @click="setTheme('dark')" class="w-full px-4 py-3 bg-slate-700 text-white font-bold rounded-md">Dunkel</button>
<button @click="setTheme('system')" class="w-full mt-2 text-sm text-slate-500 dark:text-slate-300 hover:underline">Systemstandard</button>
</div>
</div>
</div>
</transition>
<transition name="fade">
<div v-if="installModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-md max-h-[80vh] flex flex-col">
<div class="flex justify-between items-center mb-4 flex-shrink-0 dark:text-white">
<h3 class="font-bold text-lg">App installieren</h3>
<button @click="installModal.show = false" class="p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">×</button>
</div>
<div class="overflow-y-auto text-sm text-slate-700 dark:text-slate-200 space-y-6">
<div>
<h4 class="font-bold text-base mb-2 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
iPhone & iPad (mit Safari)
</h4>
<ol class="list-decimal list-inside space-y-1 pl-2">
<li>Öffnen Sie diese Webseite im <strong>Safari</strong>-Browser.</li>
<li>Tippen Sie auf das "Teilen"-Symbol (das Quadrat mit dem Pfeil nach oben).</li>
<li>Scrollen Sie nach unten und wählen Sie <strong>"Zum Home-Bildschirm"</strong>.</li>
<li>Bestätigen Sie mit "Hinzufügen". Die App erscheint nun auf Ihrem Startbildschirm.</li>
</ol>
</div>
<div>
<h4 class="font-bold text-base mb-2 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6zM3.5 9h17M3.5 15h17"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.5v15"></path></svg>
Android (mit Chrome)
</h4>
<ol class="list-decimal list-inside space-y-1 pl-2">
<li>Öffnen Sie diese Webseite im <strong>Chrome</strong>-Browser.</li>
<li>Tippen Sie auf die drei Punkte oben rechts, um das Menü zu öffnen.</li>
<li>Wählen Sie <strong>"App installieren"</strong> oder <strong>"Zum Startbildschirm hinzufügen"</strong>.</li>
<li>Bestätigen Sie die Installation. Die App erscheint nun auf Ihrem Startbildschirm.</li>
</ol>
</div>
</div>
<div class="mt-6 text-right flex-shrink-0">
<button @click="installModal.show = false" class="px-4 py-2 bg-primary text-white rounded-md">Verstanden</button>
</div>
</div>
</div>
</transition>
<div v-if="fullscreenViewer.show" @click="fullscreenViewer.show = false" class="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-2">
<button @click="fullscreenViewer.show = false" class="absolute top-2 right-2 p-2 bg-white/20 rounded-full text-white"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
<template v-if="fullscreenViewer.item.isPdf">
<iframe :src="'/File/show?id=' + fullscreenViewer.item.fileId" class="w-full h-full border-0"></iframe>
</template>
<template v-else>
<img :src="'/File/show?id=' + fullscreenViewer.item.fileId" class="max-w-full max-h-full object-contain">
</template>
</div>
</div>
`
});
app.mount('#app');
</script>
<script src="/js/pages/WorkorderBase/WorkorderServiceWorker.js"></script>
</body>
</html>
+14 -3
View File
@@ -23,6 +23,7 @@ $texts = [
'amount' => 'Menge',
'unit' => 'Einheit',
'unitPrice' => 'Einzelpreis',
'discount' => 'Rabatt',
'totalPrice' => 'Gesamtpreis'
],
'summary' => [
@@ -32,6 +33,7 @@ $texts = [
'total' => 'Gesamtbetrag',
'alternativeTotal' => 'Summe Alternativpositionen'
],
'purpose' => 'Zweck / Projekt',
'alternativeHeader' => 'Alternativpositionen',
'notes' => 'Anmerkungen & Konditionen',
'defaultOfferText' => 'Vielen Dank für Ihre Anfrage. Es gelten unsere Allgemeinen Geschäftsbedingungen.',
@@ -70,11 +72,11 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<style>
body { font-family: "Open Sans", sans-serif, Verdana; font-size: 10px; color: #333; }
h1 { text-align: center; color: #005384; font-size: 18px; margin-bottom: 20px; }
.header-info table { width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 20px; }
.header-info table { width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 12px; }
.header-info td { padding: 2px 5px; }
.header-info .label { font-weight: bold; text-align: right; padding-right: 10px; width: 120px; }
#positionsTable { width: 100%; border-collapse: collapse; margin-top: 15px; margin-bottom: 15px; }
#positionsTable { width: 100%; border-collapse: collapse; margin-top: 8px; margin-bottom: 15px; }
#positionsTable th { border-bottom: 2px solid #005384; padding: 8px 4px; text-align: left; background-color: #f2f2f2; font-size: 10px; }
#positionsTable td { border-bottom: 1px solid #e1e1e1; padding: 6px 4px; vertical-align: top; }
@@ -82,6 +84,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
#positionsTable th.amount, #positionsTable td.amount { text-align: right; width: 50px;}
#positionsTable th.unit, #positionsTable td.unit { text-align: center; width: 40px;}
#positionsTable th.price, #positionsTable td.price { text-align: right; width: 80px;}
#positionsTable th.discount, #positionsTable td.discount { text-align: right; width: 50px;}
#positionsTable th.total, #positionsTable td.total { text-align: right; width: 90px;}
.position-group-header td { background-color: #e8f0f8; font-weight: bold; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 4px; }
@@ -123,6 +126,12 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<td class="label"><?= $text['validUntilLabel'] ?></td>
<td><?= $formattedValidUntil ?></td>
</tr>
<?php if (!empty($offer->purpose)): ?>
<tr>
<td class="label" style="text-align: left; vertical-align: top; padding-top: 12px;"><?= $text['purpose'] ?></td>
<td colspan="3" style="padding-top: 12px;"><?= nl2br(htmlspecialchars($offer->purpose)) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
@@ -134,6 +143,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<th class="amount"><?= $text['table']['amount'] ?></th>
<th class="unit"><?= $text['table']['unit'] ?></th>
<th class="price"><?= $text['table']['unitPrice'] ?></th>
<th class="discount"><?= $text['table']['discount'] ?></th>
<th class="total"><?= $text['table']['totalPrice'] ?></th>
</tr>
</thead>
@@ -144,7 +154,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
$isAlternativeGroup = ($groupName === 'Alternativpositionen');
if (!empty($groupName)): ?>
<tr class="position-group-header <?= $isAlternativeGroup ? 'alternative-group-header' : '' ?>">
<td colspan="6"><?= htmlspecialchars($isAlternativeGroup ? $text['alternativeHeader'] : $groupName) ?></td>
<td colspan="7"><?= htmlspecialchars($isAlternativeGroup ? $text['alternativeHeader'] : $groupName) ?></td>
</tr>
<?php endif;
@@ -163,6 +173,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate));
<td class="amount"><?= number_format($p['amount'], 2, ',', '.') ?></td>
<td class="unit"><?= htmlspecialchars($p['articleUnit']) ?></td>
<td class="price"><?= formatPrice($p['price'], '€') ?></td>
<td class="price"><?= htmlspecialchars($p['discount'] . '%') ?></td>
<td class="total"><?= formatPrice($p['totalPrice'], '€') ?></td>
</tr>
<?php endforeach; ?>
+2 -2
View File
@@ -141,8 +141,8 @@
<?php if($me->is(["Admin","netowner","lineplanner","lineworker"]) && $me->is("employee")): ?><li><a href="<?=self::getUrl("FiberPlanPipe")?>"><i class="fas fa-pipe text-info pl-1"></i> Rohrverzeichnis</a></li><?php endif; ?>
<?php if($me->is(["Admin","netowner","lineplanner","lineworker"]) && $me->is("employee")): ?><li><a href="<?=self::getUrl("FiberPlanCable")?>"><i class="fa-solid fa-timeline text-info "></i> Kabelverzeichnis</a></li><?php endif; ?>
<!-- add a new line Arbeitsaufträge for RMLCompany, add a new line Arbeitsaufträge-Management for RMLAdmin -->
<?php if($me->can("RMLCompany")): ?><li><a href="<?=self::getUrl("RMLWorkorderCompany")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge</a></li><?php endif; ?>
<?php if($me->can("RMLAdmin")): ?><li><a href="<?=self::getUrl("RMLWorkorderAdmin")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge-Management</a></li><?php endif; ?>
<?php if($me->can("RMLCompany")): ?><li><a href="<?=self::getUrl("WorkorderCompany")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge</a></li><?php endif; ?>
<?php if($me->can("RMLAdmin")): ?><li><a href="<?=self::getUrl("WorkorderAdmin")?>"><i class="far fa-fw fa-clipboard-question text-info"></i> Arbeitsaufträge-Management</a></li><?php endif; ?>
</ul>
</li>
<?php endif; ?>