feat: Adressbuch mit Kundennummer, Update-Flow und interne Einträge

- Menüpunkt "Kunden" in "Adressbuch" umbenannt und App-Label
  "Verfügbare Jobs" zu "Auftragsliste" geändert (alle 10 Sprachen)
- Fortlaufende Kundennummer (usrId) ab 10000 über neuen
  SequenceGeneratorService und Counter-Dokument in misc-Collection
- Abholung/Lieferstation-Dialog: Änderungen an verknüpften
  Stammdaten aktualisieren den bestehenden Adressbuch-Eintrag
  statt einen neuen zu erzeugen; Checkbox-Label wechselt zu
  "Adresse im Adressbuch aktualisieren"
- Geänderte Adressen ohne Checkbox werden als interner Customer
  (internal=true) gesichert und im Adressbuch ausgeblendet
- E-Mail in AddCustomer und in Stations-Dialogen kein Pflichtfeld
  mehr; "(Login)" aus profile.email entfernt
- Manuelles Beenden eines Auftrags öffnet neue Seite
  JobManualCompleteView statt eines Dialogs
This commit is contained in:
2026-04-20 12:42:56 +02:00
parent 6e8bedd9b4
commit 704d1e7378
42 changed files with 720 additions and 196 deletions

View File

@@ -8,6 +8,7 @@ import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.model.task.SignatureTask;
import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.CargoItemRepository;
@@ -133,6 +134,14 @@ public class MessageController {
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
for (BaseTask task : tasks) {
if (task instanceof SignatureTask signatureTask && task.getId() != null) {
List<Signature> signatures = signatureRepository.findByTaskIdOrderByCreatedAtDesc(task.getId());
if (!signatures.isEmpty()) {
signatureTask.setNote(signatures.get(0).getNote());
}
}
}
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
}).toList();
@@ -246,13 +255,18 @@ public class MessageController {
Object extra = payload.get("extraData");
if (extra instanceof Map<?, ?> extraData) {
Object signatureSvgObj = extraData.get("signatureSvg");
Object signatureNoteObj = extraData.get("signatureNote");
String signatureNote = signatureNoteObj instanceof String s ? s : null;
if (signatureSvgObj instanceof String signatureSvg) {
if (!signatureSvg.isBlank()) {
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
completedBy);
signatureNote, completedBy);
signatureRepository.save(signatureEntry);
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
if (signatureNote != null && !signatureNote.isBlank()) {
extraDataSummary += ", Bemerkung: " + signatureNote;
}
} else {
extraDataSummary = "Leere Unterschrift";
}

View File

@@ -0,0 +1,13 @@
package de.assecutor.votianlt.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "misc")
public class Counter {
@Id
private String id;
private long sequence;
}

View File

@@ -53,4 +53,10 @@ public class Customer {
@Field("owner")
private ObjectId owner;
@Field("internal")
private boolean internal;
@Field("usrId")
private Integer usrId;
}

View File

@@ -20,6 +20,7 @@ public class Signature {
private ObjectId taskId;
private String signatureSvg;
private String note;
private LocalDateTime createdAt;
private String completedBy;
@@ -35,4 +36,9 @@ public class Signature {
this.signatureSvg = signatureSvg;
this.completedBy = completedBy;
}
public Signature(ObjectId taskId, String signatureSvg, String note, String completedBy) {
this(taskId, signatureSvg, completedBy);
this.note = note;
}
}

View File

@@ -1,14 +1,20 @@
package de.assecutor.votianlt.model.task;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Transient;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SignatureTask extends BaseTask {
@Transient
@JsonIgnore
private String note;
@Override
public String getTaskType() {
return "SIGNATURE";
@@ -21,11 +27,17 @@ public class SignatureTask extends BaseTask {
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
return new TaskSpecificData(note);
}
@Data
@NoArgsConstructor
public class TaskSpecificData {
public String taskType = getTaskType();
// No specific data for signature task
public String note;
public TaskSpecificData(String note) {
this.note = note;
}
}
}

View File

@@ -56,10 +56,28 @@ public class DeliveryStationDialog extends Dialog {
private String zip;
private String city;
private boolean saveAddress;
private boolean addressDiffersFromCustomer;
private org.bson.types.ObjectId customerId;
public boolean isAddressDiffersFromCustomer() {
return addressDiffersFromCustomer;
}
public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) {
this.addressDiffersFromCustomer = addressDiffersFromCustomer;
}
private List<BaseTask> tasks = new ArrayList<>();
private boolean addressValidatedByGoogle;
private AddressValidationResult addressValidationResult;
public org.bson.types.ObjectId getCustomerId() {
return customerId;
}
public void setCustomerId(org.bson.types.ObjectId customerId) {
this.customerId = customerId;
}
public boolean isAddressValidatedByGoogle() {
return addressValidatedByGoogle;
}
@@ -214,6 +232,7 @@ public class DeliveryStationDialog extends Dialog {
private final DeliveryStationTile.TranslationHelper translationHelper;
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
private org.bson.types.ObjectId selectedCustomerId;
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
@@ -512,6 +531,13 @@ public class DeliveryStationDialog extends Dialog {
zip.setValue(data.getZip());
if (data.getCity() != null)
city.setValue(data.getCity());
selectedCustomerId = data.getCustomerId();
if (selectedCustomerId == null && customerSelectedFromOptions) {
Customer matched = companyAddressOptions.get(companyOption);
if (matched != null) {
selectedCustomerId = matched.getId();
}
}
saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress());
updateSaveAddressState();
@@ -548,10 +574,43 @@ public class DeliveryStationDialog extends Dialog {
data.setZip(zip.getValue());
data.setCity(city.getValue());
data.setSaveAddress(saveAddress.getValue());
data.setCustomerId(selectedCustomerId);
data.setAddressDiffersFromCustomer(computeAddressDiffers());
data.setTasks(new ArrayList<>(tasksState));
return data;
}
private boolean computeAddressDiffers() {
boolean hasAnyValue = !isBlank(resolveCompanyValue(company.getValue())) || !isBlank(firstName.getValue())
|| !isBlank(lastName.getValue()) || !isBlank(phone.getValue()) || !isBlank(mail.getValue())
|| !isBlank(street.getValue()) || !isBlank(houseNumber.getValue())
|| !isBlank(addressAddition.getValue()) || !isBlank(zip.getValue()) || !isBlank(city.getValue());
if (!hasAnyValue) {
return false;
}
if (selectedCustomerId == null) {
return true;
}
Customer linked = findCustomerById(selectedCustomerId);
return linked == null || !matchesCurrentCustomer(linked);
}
private Customer findCustomerById(org.bson.types.ObjectId id) {
if (id == null) {
return null;
}
for (Customer c : companyAddressOptions.values()) {
if (c != null && id.equals(c.getId())) {
return c;
}
}
return null;
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private boolean validateRequiredFields() {
// Address tab validation
boolean addressValid = true;
@@ -601,11 +660,9 @@ public class DeliveryStationDialog extends Dialog {
String value = mail.getValue();
String normalizedValue = value == null ? "" : value.trim();
boolean empty = normalizedValue.isEmpty();
boolean required = Boolean.TRUE.equals(saveAddress.getValue());
boolean invalid = !empty && !normalizedValue.contains("@");
boolean hasError = invalid || (required && empty);
applyErrorStyling(mail, hasError);
return !hasError;
applyErrorStyling(mail, invalid);
return !invalid;
}
private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) {
@@ -648,10 +705,12 @@ public class DeliveryStationDialog extends Dialog {
companyField.addValueChangeListener(event -> {
Customer customer = companyAddressOptions.get(event.getValue());
if (customer == null) {
selectedCustomerId = null;
updateSaveAddressState();
return;
}
selectedCustomerId = customer.getId();
if (customer.getTitle() != null
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
@@ -680,27 +739,34 @@ public class DeliveryStationDialog extends Dialog {
companyField.addCustomValueSetListener(event -> {
companyField.setValue(event.getDetail());
selectedCustomerId = null;
updateSaveAddressState();
});
}
private void updateSaveAddressState() {
Customer selectedCustomer = companyAddressOptions.get(company.getValue());
boolean customerSelectedFromOptions = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer);
boolean customerDataMatches = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer);
if (customerSelectedFromOptions) {
if (customerDataMatches) {
saveAddress.setValue(false);
saveAddress.setEnabled(false);
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
updateMailRequirement();
return;
}
saveAddress.setEnabled(true);
if (selectedCustomerId != null) {
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update"));
} else {
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
}
updateMailRequirement();
}
private void updateMailRequirement() {
mail.setRequiredIndicatorVisible(Boolean.TRUE.equals(saveAddress.getValue()));
mail.setRequiredIndicatorVisible(false);
}
private String buildCompanyAddressLabel(Customer customer) {
@@ -1343,10 +1409,14 @@ public class DeliveryStationDialog extends Dialog {
break;
case SIGNATURE:
Span info = new Span(translationHelper.getTranslation("addjob.tasks.signature.noconfig"));
info.getStyle().set("color", "var(--lumo-secondary-text-color)");
info.getStyle().set("font-style", "italic");
configContainer.add(info);
TextField signatureNoteField = new TextField(
translationHelper.getTranslation("addjob.tasks.signature.notelabel"));
signatureNoteField.setPlaceholder(
translationHelper.getTranslation("addjob.tasks.signature.notelabel.placeholder"));
signatureNoteField.setWidthFull();
signatureNoteField.setValue(task.getDescription() != null ? task.getDescription() : "");
signatureNoteField.addValueChangeListener(ev -> task.setDescription(ev.getValue()));
configContainer.add(signatureNoteField);
break;
case TODOLIST:

View File

@@ -228,6 +228,25 @@ public class PickupStationDialog extends Dialog {
public void setCargoItems(List<CargoItem> cargoItems) {
this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>();
}
private org.bson.types.ObjectId customerId;
private boolean addressDiffersFromCustomer;
public org.bson.types.ObjectId getCustomerId() {
return customerId;
}
public void setCustomerId(org.bson.types.ObjectId customerId) {
this.customerId = customerId;
}
public boolean isAddressDiffersFromCustomer() {
return addressDiffersFromCustomer;
}
public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) {
this.addressDiffersFromCustomer = addressDiffersFromCustomer;
}
}
public interface SaveListener {
@@ -250,6 +269,7 @@ public class PickupStationDialog extends Dialog {
private final ComboBox<String> customerComboBox;
private final Map<String, Customer> customerLabelMap = new LinkedHashMap<>();
private final Map<String, Customer> companyCustomerMap = new LinkedHashMap<>();
private org.bson.types.ObjectId selectedCustomerId;
private DatePicker appointmentDatePicker;
private TimePicker appointmentTimePicker;
private Checkbox digitalProcessingCheckbox;
@@ -431,14 +451,17 @@ public class PickupStationDialog extends Dialog {
customerComboBox.addValueChangeListener(ev -> {
String selected = ev.getValue();
if (selected == null) {
selectedCustomerId = null;
updateSaveAddressState();
return;
}
Customer c = customerLabelMap.get(selected);
if (c == null) {
selectedCustomerId = null;
updateSaveAddressState();
return;
}
selectedCustomerId = c.getId();
if (c.getCompanyName() != null)
company.setValue(c.getCompanyName());
else
@@ -663,6 +686,15 @@ public class PickupStationDialog extends Dialog {
}
}
if (data.getCustomerId() != null) {
selectedCustomerId = data.getCustomerId();
} else {
Customer matched = customerLabelMap.get(data.getCustomerSelection());
if (matched == null) {
matched = companyCustomerMap.get(normalizeValue(data.getCompany()));
}
selectedCustomerId = matched != null ? matched.getId() : null;
}
saveAddress.setValue(data.isSaveAddress());
updateSaveAddressState();
}
@@ -681,6 +713,8 @@ public class PickupStationDialog extends Dialog {
data.setZip(zip.getValue());
data.setCity(city.getValue());
data.setSaveAddress(saveAddress.getValue());
data.setCustomerId(selectedCustomerId);
data.setAddressDiffersFromCustomer(computeAddressDiffers());
data.setCustomerSelection(customerComboBox.getValue());
if (appointmentDatePicker != null) {
data.setAppointmentDate(appointmentDatePicker.getValue());
@@ -820,6 +854,7 @@ public class PickupStationDialog extends Dialog {
companyField.addValueChangeListener(event -> {
String selectedCompany = event.getValue();
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
selectedCustomerId = null;
updateSaveAddressState();
return;
}
@@ -829,6 +864,7 @@ public class PickupStationDialog extends Dialog {
if (matchingCustomer.isPresent()) {
Customer customer = matchingCustomer.get();
selectedCustomerId = customer.getId();
if (customer.getTitle() != null
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
@@ -859,6 +895,7 @@ public class PickupStationDialog extends Dialog {
companyField.addCustomValueSetListener(event -> {
companyField.setValue(event.getDetail());
selectedCustomerId = null;
updateSaveAddressState();
});
}
@@ -866,17 +903,23 @@ public class PickupStationDialog extends Dialog {
private void updateSaveAddressState() {
Customer selectedCustomer = customerLabelMap.get(customerComboBox.getValue());
Customer selectedCompanyCustomer = companyCustomerMap.get(normalizeValue(company.getValue()));
boolean existingCustomerSelected = selectedCustomer != null && matchesCustomer(selectedCustomer);
boolean existingCompanySelected = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer);
boolean customerDataMatches = selectedCustomer != null && matchesCustomer(selectedCustomer);
boolean companyDataMatches = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer);
if (existingCustomerSelected || existingCompanySelected) {
if (customerDataMatches || companyDataMatches) {
saveAddress.setValue(false);
saveAddress.setEnabled(false);
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
updateMailRequirement();
return;
}
saveAddress.setEnabled(true);
if (selectedCustomerId != null) {
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update"));
} else {
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
}
updateMailRequirement();
}
@@ -906,6 +949,40 @@ public class PickupStationDialog extends Dialog {
return value == null ? "" : value.trim();
}
private boolean computeAddressDiffers() {
boolean hasAnyValue = !normalizeValue(company.getValue()).isEmpty()
|| !normalizeValue(firstName.getValue()).isEmpty() || !normalizeValue(lastName.getValue()).isEmpty()
|| !normalizeValue(phone.getValue()).isEmpty() || !normalizeValue(mail.getValue()).isEmpty()
|| !normalizeValue(street.getValue()).isEmpty() || !normalizeValue(houseNumber.getValue()).isEmpty()
|| !normalizeValue(addressAddition.getValue()).isEmpty() || !normalizeValue(zip.getValue()).isEmpty()
|| !normalizeValue(city.getValue()).isEmpty();
if (!hasAnyValue) {
return false;
}
if (selectedCustomerId == null) {
return true;
}
Customer linked = findCustomerById(selectedCustomerId);
return linked == null || !matchesCustomer(linked);
}
private Customer findCustomerById(org.bson.types.ObjectId id) {
if (id == null) {
return null;
}
for (Customer c : customerLabelMap.values()) {
if (c != null && id.equals(c.getId())) {
return c;
}
}
for (Customer c : companyCustomerMap.values()) {
if (c != null && id.equals(c.getId())) {
return c;
}
}
return null;
}
// ============================================
// Appointments & Processing Tab
// ============================================

View File

@@ -13,4 +13,6 @@ public interface CustomerRepository extends MongoRepository<Customer, ObjectId>
Slice<Customer> findAllBy(Pageable pageable);
List<Customer> findByOwner(ObjectId owner);
List<Customer> findByOwnerAndInternalFalse(ObjectId owner);
}

View File

@@ -12,10 +12,13 @@ import org.springframework.transaction.annotation.Transactional;
public class AddCustomerService {
private final AddCustomerRepository addCustomerRepository;
private final SecurityService securityService;
private final SequenceGeneratorService sequenceGeneratorService;
AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService) {
AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService,
SequenceGeneratorService sequenceGeneratorService) {
this.addCustomerRepository = addCustomerRepository;
this.securityService = securityService;
this.sequenceGeneratorService = sequenceGeneratorService;
}
public void addCustomer(Customer customer) {
@@ -25,6 +28,35 @@ public class AddCustomerService {
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
customer.setCreatedBy(currentUser.getId());
customer.setOwner(currentUser.getId());
if (customer.getUsrId() == null) {
customer.setUsrId(sequenceGeneratorService.nextCustomerNumber());
}
addCustomerRepository.save(customer);
}
public void addInternalCustomer(Customer customer) {
if (customer == null) {
return;
}
customer.setId(null);
customer.setInternal(true);
addCustomer(customer);
}
public void updateCustomer(Customer customer) {
if (customer == null || customer.getId() == null) {
throw new IllegalArgumentException("Kunden-ID fehlt");
}
validateCustomer(customer);
Customer existing = addCustomerRepository.findById(customer.getId())
.orElseThrow(() -> new IllegalArgumentException("Kunde nicht gefunden"));
customer.setCreatedBy(existing.getCreatedBy());
customer.setOwner(existing.getOwner());
if (customer.getUsrId() == null) {
customer.setUsrId(existing.getUsrId());
}
addCustomerRepository.save(customer);
}
@@ -35,13 +67,10 @@ public class AddCustomerService {
}
String mail = customer.getMail() != null ? customer.getMail().trim() : "";
if (mail.isEmpty()) {
throw new IllegalArgumentException("E-Mail-Adresse ist ein Pflichtfeld");
}
if (!mail.contains("@")) {
if (!mail.isEmpty() && !mail.contains("@")) {
throw new IllegalArgumentException("Bitte geben Sie eine gültige E-Mail-Adresse ein");
}
customer.setMail(mail);
customer.setMail(mail.isEmpty() ? null : mail);
}
}

View File

@@ -32,7 +32,7 @@ public class CustomerService {
public List<Customer> findAllForCurrentOwner() {
ObjectId ownerId = securityService.getCurrentUserId();
return todoRepository.findByOwner(ownerId);
return todoRepository.findByOwnerAndInternalFalse(ownerId);
}
public Customer save(Customer customer) {

View File

@@ -0,0 +1,54 @@
package de.assecutor.votianlt.pages.service;
import de.assecutor.votianlt.model.Counter;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
@Service
public class SequenceGeneratorService {
public static final String CUSTOMER_NUMBER_SEQ = "customerNumber";
public static final int CUSTOMER_NUMBER_START = 10000;
private final MongoTemplate mongoTemplate;
public SequenceGeneratorService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public int nextCustomerNumber() {
return (int) nextSequence(CUSTOMER_NUMBER_SEQ, CUSTOMER_NUMBER_START);
}
private long nextSequence(String sequenceId, long startValue) {
ensureInitialized(sequenceId, startValue - 1);
Counter updated = mongoTemplate.findAndModify(
Query.query(Criteria.where("_id").is(sequenceId)),
new Update().inc("sequence", 1),
FindAndModifyOptions.options().returnNew(true),
Counter.class);
return updated != null ? updated.getSequence() : startValue;
}
private void ensureInitialized(String sequenceId, long initialValue) {
boolean exists = mongoTemplate.exists(
Query.query(Criteria.where("_id").is(sequenceId)),
Counter.class);
if (exists) {
return;
}
Counter counter = new Counter();
counter.setId(sequenceId);
counter.setSequence(initialValue);
try {
mongoTemplate.insert(counter);
} catch (DuplicateKeyException ignored) {
// Ein anderer Thread hat den Counter gleichzeitig angelegt — passt.
}
}
}

View File

@@ -81,9 +81,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
fax = new TextField(getTranslation("profile.fax"));
fax.setWidthFull();
// E-Mail (Pflichtfeld)
// E-Mail (optional)
mail = new TextField(getTranslation("profile.email"));
mail.setRequiredIndicatorVisible(true);
mail.setWidthFull();
mail.addBlurListener(e -> validateEmail());
@@ -179,8 +178,9 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
binder.forField(fax).bind(Customer::getFax, Customer::setFax);
binder.forField(mail).asRequired(getTranslation("profile.validation.email.required"))
.withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid"))
binder.forField(mail)
.withValidator(email -> email == null || email.isBlank() || email.contains("@"),
getTranslation("profile.validation.email.invalid"))
.bind(Customer::getMail, Customer::setMail);
binder.forField(street).asRequired(getTranslation("profile.validation.street.required"))
@@ -247,10 +247,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
private void validateEmail() {
String value = mail.getValue();
if (value == null || value.trim().isEmpty()) {
mail.setInvalid(true);
mail.setErrorMessage(getTranslation("profile.validation.email.required"));
} else if (!value.contains("@")) {
if (value != null && !value.trim().isEmpty() && !value.contains("@")) {
mail.setInvalid(true);
mail.setErrorMessage(getTranslation("profile.validation.email.invalid"));
} else {

View File

@@ -137,12 +137,16 @@ public class AddJobView extends Main implements HasDynamicTitle {
private TextField pickupZip;
private TextField pickupCity;
private Checkbox savePickupAddress;
private org.bson.types.ObjectId pickupCustomerId;
private boolean pickupAddressDiffers;
// Delivery stations as tiles in a 3x3 grid (max 7 delivery + 1 pickup + 1 plus
// = 9)
private final List<StationTile> deliveryStationTilesList = new ArrayList<>();
private final List<DeliveryStation> deliveryStationsState = new ArrayList<>();
private final List<Boolean> deliveryStationsSaveAddress = new ArrayList<>();
private final List<org.bson.types.ObjectId> deliveryStationsCustomerId = new ArrayList<>();
private final List<Boolean> deliveryStationsAddressDiffers = new ArrayList<>();
private final List<String> deliveryStationsMailState = new ArrayList<>();
private final List<Div> deliveryStationSlotList = new ArrayList<>();
private final List<Span> deliveryStationDistanceChips = new ArrayList<>();
@@ -721,6 +725,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
// Add empty state for this station
deliveryStationsState.add(new DeliveryStation());
deliveryStationsSaveAddress.add(true);
deliveryStationsCustomerId.add(null);
deliveryStationsAddressDiffers.add(false);
deliveryStationsMailState.add(null);
deliveryStationsValidatedByGoogle.add(false);
@@ -769,6 +775,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
deliveryStationTilesList.remove(removeIdx);
deliveryStationsState.remove(removeIdx);
deliveryStationsSaveAddress.remove(removeIdx);
deliveryStationsCustomerId.remove(removeIdx);
deliveryStationsAddressDiffers.remove(removeIdx);
deliveryStationsMailState.remove(removeIdx);
deliveryStationsValidatedByGoogle.remove(removeIdx);
deliveryStationTasksState.remove(removeIdx);
@@ -867,6 +875,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
pickupZip.setValue(data.getZip() != null ? data.getZip() : "");
pickupCity.setValue(data.getCity() != null ? data.getCity() : "");
savePickupAddress.setValue(data.isSaveAddress());
pickupCustomerId = data.getCustomerId();
pickupAddressDiffers = data.isAddressDiffersFromCustomer();
// Sync appointment fields for binder/submit
pickupDate.setValue(data.getAppointmentDate());
@@ -913,6 +923,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
currentData.setZip(pickupZip.getValue());
currentData.setCity(pickupCity.getValue());
currentData.setSaveAddress(savePickupAddress.getValue());
currentData.setCustomerId(pickupCustomerId);
currentData.setCustomerSelection(customerSelection.getValue());
// Pre-fill pickup-specific fields
currentData.setAppointmentDate(pickupDate.getValue());
@@ -1137,6 +1148,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
station.setCity(data.getCity());
station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>());
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
while (deliveryStationsCustomerId.size() <= idx) {
deliveryStationsCustomerId.add(null);
}
deliveryStationsCustomerId.set(idx, data.getCustomerId());
while (deliveryStationsAddressDiffers.size() <= idx) {
deliveryStationsAddressDiffers.add(false);
}
deliveryStationsAddressDiffers.set(idx, data.isAddressDiffersFromCustomer());
deliveryStationsMailState.set(idx, trimToNull(data.getMail()));
// Store tasks for this delivery station
@@ -1182,6 +1201,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
currentData.setZip(station.getZip());
currentData.setCity(station.getCity());
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
if (actualIndex < deliveryStationsCustomerId.size()) {
currentData.setCustomerId(deliveryStationsCustomerId.get(actualIndex));
}
if (actualIndex < deliveryStationsValidatedByGoogle.size()) {
currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex));
}
@@ -1817,39 +1839,60 @@ public class AddJobView extends Main implements HasDynamicTitle {
return;
}
// NEU: Kunden anlegen, wenn Checkboxen aktiviert
// Kunden anlegen/aktualisieren bzw. intern sichern
Customer pickupCustomer = new Customer();
pickupCustomer.setCompanyName(pickupCompany.getValue());
pickupCustomer.setTitle(pickupSalutation.getValue());
pickupCustomer.setFirstname(pickupFirstName.getValue());
pickupCustomer.setLastName(pickupLastName.getValue());
pickupCustomer.setTelephone(pickupPhone.getValue());
pickupCustomer.setMail(pickupMail);
pickupCustomer.setStreet(pickupStreet.getValue());
pickupCustomer.setHouseNumber(pickupHouseNumber.getValue());
pickupCustomer.setAddressAddition(pickupAddressAddition.getValue());
pickupCustomer.setZip(pickupZip.getValue());
pickupCustomer.setCity(pickupCity.getValue());
if (savePickupAddress.getValue()) {
Customer pickupCustomer = new Customer();
pickupCustomer.setCompanyName(pickupCompany.getValue());
pickupCustomer.setTitle(pickupSalutation.getValue());
pickupCustomer.setFirstname(pickupFirstName.getValue());
pickupCustomer.setLastName(pickupLastName.getValue());
pickupCustomer.setTelephone(pickupPhone.getValue());
pickupCustomer.setMail(pickupMail);
pickupCustomer.setStreet(pickupStreet.getValue());
pickupCustomer.setHouseNumber(pickupHouseNumber.getValue());
pickupCustomer.setAddressAddition(pickupAddressAddition.getValue());
pickupCustomer.setZip(pickupZip.getValue());
pickupCustomer.setCity(pickupCity.getValue());
addCustomerService.addCustomer(pickupCustomer);
if (pickupCustomerId != null) {
pickupCustomer.setId(pickupCustomerId);
addCustomerService.updateCustomer(pickupCustomer);
} else {
addCustomerService.addCustomer(pickupCustomer);
}
} else if (pickupAddressDiffers) {
addCustomerService.addInternalCustomer(pickupCustomer);
}
// Save delivery station addresses as customers if checkbox is checked
// Delivery-Stationen: anlegen, aktualisieren oder als intern sichern
for (int i = 0; i < deliveryStationsState.size(); i++) {
if (i < deliveryStationsSaveAddress.size() && deliveryStationsSaveAddress.get(i)) {
DeliveryStation ds = deliveryStationsState.get(i);
Customer deliveryCustomer = new Customer();
deliveryCustomer.setCompanyName(ds.getCompany());
deliveryCustomer.setTitle(ds.getSalutation());
deliveryCustomer.setFirstname(ds.getFirstName());
deliveryCustomer.setLastName(ds.getLastName());
deliveryCustomer.setTelephone(ds.getPhone());
deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null);
deliveryCustomer.setStreet(ds.getStreet());
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
deliveryCustomer.setAddressAddition(ds.getAddressAddition());
deliveryCustomer.setZip(ds.getZip());
deliveryCustomer.setCity(ds.getCity());
addCustomerService.addCustomer(deliveryCustomer);
DeliveryStation ds = deliveryStationsState.get(i);
Customer deliveryCustomer = new Customer();
deliveryCustomer.setCompanyName(ds.getCompany());
deliveryCustomer.setTitle(ds.getSalutation());
deliveryCustomer.setFirstname(ds.getFirstName());
deliveryCustomer.setLastName(ds.getLastName());
deliveryCustomer.setTelephone(ds.getPhone());
deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null);
deliveryCustomer.setStreet(ds.getStreet());
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
deliveryCustomer.setAddressAddition(ds.getAddressAddition());
deliveryCustomer.setZip(ds.getZip());
deliveryCustomer.setCity(ds.getCity());
boolean saveRequested = i < deliveryStationsSaveAddress.size()
&& deliveryStationsSaveAddress.get(i);
org.bson.types.ObjectId existingId = i < deliveryStationsCustomerId.size()
? deliveryStationsCustomerId.get(i)
: null;
boolean addressDiffers = i < deliveryStationsAddressDiffers.size()
&& deliveryStationsAddressDiffers.get(i);
if (saveRequested) {
if (existingId != null) {
deliveryCustomer.setId(existingId);
addCustomerService.updateCustomer(deliveryCustomer);
} else {
addCustomerService.addCustomer(deliveryCustomer);
}
} else if (addressDiffers) {
addCustomerService.addInternalCustomer(deliveryCustomer);
}
}
@@ -2119,6 +2162,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
deliveryStationTilesList.remove(idx);
deliveryStationsState.remove(idx);
deliveryStationsSaveAddress.remove(idx);
deliveryStationsCustomerId.remove(idx);
deliveryStationsAddressDiffers.remove(idx);
deliveryStationsMailState.remove(idx);
deliveryStationsValidatedByGoogle.remove(idx);
deliveryStationTasksState.remove(idx);

View File

@@ -36,7 +36,8 @@ public class CustomersView extends Main implements HasDynamicTitle {
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
todoGrid = new Grid<>();
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream()
.filter(c -> !c.isInternal()));
todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"));
todoGrid.setSizeFull();
todoGrid.addClassName("data-grid");

View File

@@ -0,0 +1,152 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobHistoryType;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.JobHistoryService;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import java.time.LocalDateTime;
@Route(value = "job_manual_complete", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed("USER")
@Slf4j
public class JobManualCompleteView extends Main implements HasUrlParameter<String>, HasDynamicTitle {
private final JobRepository jobRepository;
private final JobHistoryService jobHistoryService;
private final SecurityService securityService;
private final VerticalLayout content;
public JobManualCompleteView(JobRepository jobRepository, JobHistoryService jobHistoryService,
SecurityService securityService) {
this.jobRepository = jobRepository;
this.jobHistoryService = jobHistoryService;
this.securityService = securityService;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
add(new ViewToolbar(getTranslation("jobsummary.dialog.manualcomplete.title")));
content = new VerticalLayout();
content.setSpacing(true);
content.setPadding(true);
content.setWidthFull();
content.addClassNames("form-shell", "form-card");
add(content);
}
@Override
public String getPageTitle() {
return getTranslation("jobsummary.dialog.manualcomplete.title");
}
@Override
public void setParameter(BeforeEvent event, String parameter) {
content.removeAll();
if (parameter == null || parameter.isBlank()) {
content.add(new Span(getTranslation("jobhistory.error.no.id")));
return;
}
ObjectId jobId;
try {
jobId = new ObjectId(parameter);
} catch (Exception e) {
content.add(new Span(getTranslation("jobhistory.error.invalid.id", parameter)));
return;
}
Job job = jobRepository.findById(jobId).orElse(null);
if (job == null) {
content.add(new Span(getTranslation("jobhistory.error.not.found", parameter)));
return;
}
render(job);
}
private void render(Job job) {
Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber()));
warningText.getStyle().set("color", "var(--lumo-error-text-color)");
TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason"));
reasonField.setWidthFull();
reasonField.setMinHeight("100px");
reasonField.setRequired(true);
content.add(warningText, reasonField);
HorizontalLayout buttonBar = new HorizontalLayout();
buttonBar.setWidthFull();
buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END);
buttonBar.setSpacing(true);
Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"),
e -> getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())));
Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm"));
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
confirmButton.addClickListener(e -> {
String reason = reasonField.getValue();
if (reason == null || reason.trim().isEmpty()) {
reasonField.setInvalid(true);
reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required"));
return;
}
try {
JobStatus oldStatus = job.getStatus();
job.setStatus(JobStatus.COMPLETED);
job.setUpdatedAt(LocalDateTime.now());
jobRepository.save(job);
String currentUser = securityService.getCurrentUsername();
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser);
String description = String.format("Auftrag manuell beendet von %s. Begründung: %s",
currentUser, reason.trim());
jobHistoryService.logCustomEvent(job.getId(),
getTranslation("jobsummary.history.manualcomplete.reason"),
description, currentUser, JobHistoryType.STATUS_CHANGE);
Notification
.show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
} catch (Exception ex) {
Notification
.show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
buttonBar.add(cancelButton, confirmButton);
content.add(buttonBar);
}
}

View File

@@ -14,7 +14,6 @@ import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
@@ -60,8 +59,6 @@ import de.assecutor.votianlt.service.JobUpdateBroadcaster;
import de.assecutor.votianlt.service.LocationService;
import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.service.TaskAssignmentService;
import de.assecutor.votianlt.model.JobHistoryType;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import jakarta.annotation.security.RolesAllowed;
import org.bson.types.ObjectId;
@@ -91,7 +88,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final LocationService locationService;
private final ServiceRepository serviceRepository;
private final TaskAssignmentService taskAssignmentService;
private final SecurityService securityService;
@Value("${app.google.maps.api-key}")
private String googleMapsApiKey;
@@ -107,8 +103,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
MessageService messageService, JobHistoryService jobHistoryService,
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService,
SecurityService securityService) {
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) {
this.jobRepository = jobRepository;
this.cargoItemRepository = cargoItemRepository;
this.signatureRepository = signatureRepository;
@@ -121,7 +116,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
this.locationService = locationService;
this.serviceRepository = serviceRepository;
this.taskAssignmentService = taskAssignmentService;
this.securityService = securityService;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
@@ -224,7 +218,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
&& job.getStatus() != JobStatus.CANCELLED) {
manualCompleteButton = new Button(getTranslation("jobsummary.button.manualcomplete"));
manualCompleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
manualCompleteButton.addClickListener(e -> openManualCompleteDialog(job));
manualCompleteButton.addClickListener(e -> getUI()
.ifPresent(ui -> ui.navigate("job_manual_complete/" + job.getId().toHexString())));
}
// Create Job History Button for toolbar
@@ -379,75 +374,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
}
}
private void openManualCompleteDialog(Job job) {
Dialog dialog = DialogStylingHelper.createStyledDialog(
getTranslation("jobsummary.dialog.manualcomplete.title"), "560px");
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("520px");
Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber()));
warningText.getStyle().set("color", "var(--lumo-error-text-color)");
TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason"));
reasonField.setWidthFull();
reasonField.setMinHeight("100px");
reasonField.setRequired(true);
dialogContent.add(warningText, reasonField);
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
HorizontalLayout buttonBar = new HorizontalLayout();
buttonBar.setWidthFull();
buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END);
buttonBar.setSpacing(true);
Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"),
e -> dialog.close());
Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm"));
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
confirmButton.addClickListener(e -> {
String reason = reasonField.getValue();
if (reason == null || reason.trim().isEmpty()) {
reasonField.setInvalid(true);
reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required"));
return;
}
try {
JobStatus oldStatus = job.getStatus();
job.setStatus(JobStatus.COMPLETED);
job.setUpdatedAt(LocalDateTime.now());
jobRepository.save(job);
String currentUser = securityService.getCurrentUsername();
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser);
String description = String.format("Auftrag manuell beendet von %s. Begründung: %s",
currentUser, reason.trim());
jobHistoryService.logCustomEvent(job.getId(),
getTranslation("jobsummary.history.manualcomplete.reason"),
description, currentUser, JobHistoryType.STATUS_CHANGE);
dialog.close();
Notification
.show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
} catch (Exception ex) {
Notification
.show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
buttonBar.add(cancelButton, confirmButton);
dialog.getFooter().add(buttonBar);
dialog.open();
}
private VerticalLayout borderedBox() {
VerticalLayout box = new VerticalLayout();
box.addClassName("summary-card");

View File

@@ -86,7 +86,8 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
var customers = customerService.findAll();
var currentUserId = securityService.getCurrentUserId();
var ownCustomers = customers.stream()
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList();
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId))
.filter(c -> !c.isInternal()).toList();
grid.setItems(ownCustomers);
}

View File

@@ -10,4 +10,6 @@ import java.util.List;
@Repository
public interface SignatureRepository extends MongoRepository<Signature, ObjectId> {
List<Signature> findByTaskId(ObjectId taskId);
List<Signature> findByTaskIdOrderByCreatedAtDesc(ObjectId taskId);
}

View File

@@ -5,7 +5,7 @@ dialog.confirm=Bestätigen
# Navigation and Main Layout
nav.jobs=Aufträge
nav.job.create=Auftragserstellung
nav.customers=Kunden
nav.customers=Adressbuch
nav.appusers=App-Nutzer
nav.statistics=Statistiken
nav.invoices=Rechnungen
@@ -31,7 +31,7 @@ profile.lastname=Nachname
profile.phone=Telefonnummer
profile.fax=Telefon (Fax)
profile.mobile=Telefon (Mobil)
profile.email=E-Mail-Adresse (Login)*
profile.email=E-Mail-Adresse
profile.street=Straße
profile.housenr=Hausnr
profile.addressadd=Adresszusatz
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Ort (Abholung)
addjob.address.city.placeholder.delivery=Ort (Lieferung)
addjob.address.delivery.street.placeholder=Straße (Lieferung)
addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
addjob.address.save=Adresse speichern
addjob.address.save=Adresse in Adressbuch übernehmen
addjob.address.update=Adresse im Adressbuch aktualisieren
addjob.section.pickup=Abholung
addjob.section.delivery=Lieferung
addjob.stations.apply=Stationen \u00fcbernehmen
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Fotos
addjob.tasks.photo.max=Max. Fotos
addjob.tasks.barcode.min=Min. Barcodes
addjob.tasks.barcode.max=Max. Barcodes
addjob.tasks.signature.noconfig=Keine Konfiguration erforderlich
addjob.tasks.signature.notelabel=Bemerkung (optional)
addjob.tasks.signature.notelabel.placeholder=Hinweistext für die Bemerkung eingeben
addjob.tasks.todolist.title=To-Do Liste
addjob.tasks.todolist.item.placeholder=To-Do eingeben
addjob.tasks.todolist.add=To-Do hinzufügen

View File

@@ -3,7 +3,7 @@ dialog.cancel=T\u00fchista
dialog.confirm=Kinnita
nav.jobs=Tellimused
nav.job.create=Tellimuse loomine
nav.customers=Kliendid
nav.customers=Aadressiraamat
nav.appusers=\u00c4pikasutajad
nav.statistics=Statistika
nav.invoices=Arved
@@ -27,7 +27,7 @@ profile.lastname=Perekonnanimi
profile.phone=Telefoninumber
profile.fax=Telefon (faks)
profile.mobile=Telefon (mobiil)
profile.email=E-posti aadress (sisselogimine)*
profile.email=E-posti aadress
profile.street=T\u00e4nav
profile.housenr=Majanumber
profile.addressadd=Aadressi t\u00e4iend
@@ -396,7 +396,8 @@ addjob.address.city.placeholder.pickup=Asukoht (pealekorje)
addjob.address.city.placeholder.delivery=Asukoht (kohaletoimetamine)
addjob.address.delivery.street.placeholder=T\u00e4nav (kohaletoimetamine)
addjob.address.delivery.addition.placeholder=Aadressi t\u00e4iend (kohaletoimetamine)
addjob.address.save=Salvesta aadress
addjob.address.save=Lisa aadress aadressiraamatusse
addjob.address.update=Uuenda aadressi aadressiraamatus
addjob.section.pickup=Pealekorje
addjob.section.delivery=Kohaletoimetamine
addjob.stations.apply=Rakenda jaamad
@@ -462,7 +463,8 @@ addjob.tasks.photo.min=Min. fotosid
addjob.tasks.photo.max=Max. fotosid
addjob.tasks.barcode.min=Min. v\u00f6\u00f6tkoode
addjob.tasks.barcode.max=Max. v\u00f6\u00f6tkoode
addjob.tasks.signature.noconfig=Seadistamine pole vajalik
addjob.tasks.signature.notelabel=Märkus (valikuline)
addjob.tasks.signature.notelabel.placeholder=Sisestage vihje tekst märkusele
addjob.tasks.todolist.title=\u00dclesannete nimekiri
addjob.tasks.todolist.item.placeholder=Sisestage \u00fclesanne
addjob.tasks.todolist.add=Lisa \u00fclesanne

View File

@@ -5,7 +5,7 @@ dialog.confirm=Confirm
# Navigation and Main Layout
nav.jobs=Jobs
nav.job.create=Create Job
nav.customers=Customers
nav.customers=Address Book
nav.appusers=App Users
nav.statistics=Statistics
nav.invoices=Invoices
@@ -31,7 +31,7 @@ profile.lastname=Last Name
profile.phone=Phone Number
profile.fax=Phone (Fax)
profile.mobile=Phone (Mobile)
profile.email=Email Address (Login)*
profile.email=Email Address
profile.street=Street
profile.housenr=House No.
profile.addressadd=Address Suffix
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=City (Pickup)
addjob.address.city.placeholder.delivery=City (Delivery)
addjob.address.delivery.street.placeholder=Street (Delivery)
addjob.address.delivery.addition.placeholder=Address suffix (Delivery)
addjob.address.save=Save Address
addjob.address.save=Add address to address book
addjob.address.update=Update address in address book
addjob.section.pickup=Pickup
addjob.section.delivery=Delivery
addjob.stations.apply=Apply Stations
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Photos
addjob.tasks.photo.max=Max. Photos
addjob.tasks.barcode.min=Min. Barcodes
addjob.tasks.barcode.max=Max. Barcodes
addjob.tasks.signature.noconfig=No configuration required
addjob.tasks.signature.notelabel=Note (optional)
addjob.tasks.signature.notelabel.placeholder=Enter hint text for the note
addjob.tasks.todolist.title=To-Do List
addjob.tasks.todolist.item.placeholder=Enter to-do
addjob.tasks.todolist.add=Add To-Do

View File

@@ -5,7 +5,7 @@ dialog.confirm=Confirmar
# Navigation and Main Layout
nav.jobs=Pedidos
nav.job.create=Crear pedido
nav.customers=Clientes
nav.customers=Libreta de direcciones
nav.appusers=Usuarios de la app
nav.statistics=Estad\u00edsticas
nav.invoices=Facturas
@@ -31,7 +31,7 @@ profile.lastname=Apellido
profile.phone=N\u00famero de tel\u00e9fono
profile.fax=Tel\u00e9fono (Fax)
profile.mobile=Tel\u00e9fono (M\u00f3vil)
profile.email=Direcci\u00f3n de correo electr\u00f3nico (Login)*
profile.email=Direcci\u00f3n de correo electr\u00f3nico
profile.street=Calle
profile.housenr=N\u00famero
profile.addressadd=Complemento de direcci\u00f3n
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Localidad (Recogida)
addjob.address.city.placeholder.delivery=Localidad (Entrega)
addjob.address.delivery.street.placeholder=Calle (Entrega)
addjob.address.delivery.addition.placeholder=Complemento de direcci\u00f3n (Entrega)
addjob.address.save=Guardar direcci\u00f3n
addjob.address.save=A\u00f1adir direcci\u00f3n a la libreta de direcciones
addjob.address.update=Actualizar direcci\u00f3n en la libreta de direcciones
addjob.section.pickup=Recogida
addjob.section.delivery=Entrega
addjob.stations.apply=Aplicar estaciones
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=M\u00edn. fotos
addjob.tasks.photo.max=M\u00e1x. fotos
addjob.tasks.barcode.min=M\u00edn. c\u00f3digos de barras
addjob.tasks.barcode.max=M\u00e1x. c\u00f3digos de barras
addjob.tasks.signature.noconfig=No se requiere configuraci\u00f3n
addjob.tasks.signature.notelabel=Nota (opcional)
addjob.tasks.signature.notelabel.placeholder=Introducir texto de sugerencia para la nota
addjob.tasks.todolist.title=Lista de tareas pendientes
addjob.tasks.todolist.item.placeholder=Introducir tarea pendiente
addjob.tasks.todolist.add=A\u00f1adir tarea pendiente

View File

@@ -5,7 +5,7 @@ dialog.confirm=Confirmer
# Navigation and Main Layout
nav.jobs=Missions
nav.job.create=Cr\u00e9ation de mission
nav.customers=Clients
nav.customers=Carnet d'adresses
nav.appusers=Utilisateurs d'app
nav.statistics=Statistiques
nav.invoices=Factures
@@ -31,7 +31,7 @@ profile.lastname=Nom
profile.phone=Num\u00e9ro de t\u00e9l\u00e9phone
profile.fax=T\u00e9l\u00e9phone (fax)
profile.mobile=T\u00e9l\u00e9phone (mobile)
profile.email=Adresse e-mail (connexion)*
profile.email=Adresse e-mail
profile.street=Rue
profile.housenr=N\u00b0
profile.addressadd=Compl\u00e9ment d'adresse
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Ville (enl\u00e8vement)
addjob.address.city.placeholder.delivery=Ville (livraison)
addjob.address.delivery.street.placeholder=Rue (livraison)
addjob.address.delivery.addition.placeholder=Compl\u00e9ment d'adresse (livraison)
addjob.address.save=Enregistrer l'adresse
addjob.address.save=Ajouter l'adresse au carnet d'adresses
addjob.address.update=Mettre \u00e0 jour l'adresse dans le carnet d'adresses
addjob.section.pickup=Enl\u00e8vement
addjob.section.delivery=Livraison
addjob.stations.apply=Appliquer les stations
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. photos
addjob.tasks.photo.max=Max. photos
addjob.tasks.barcode.min=Min. codes-barres
addjob.tasks.barcode.max=Max. codes-barres
addjob.tasks.signature.noconfig=Aucune configuration n\u00e9cessaire
addjob.tasks.signature.notelabel=Note (optionnelle)
addjob.tasks.signature.notelabel.placeholder=Saisir le texte d'indication pour la note
addjob.tasks.todolist.title=Liste de t\u00e2ches
addjob.tasks.todolist.item.placeholder=Saisir la t\u00e2che
addjob.tasks.todolist.add=Ajouter une t\u00e2che

View File

@@ -5,7 +5,7 @@ dialog.confirm=Patvirtinti
# Navigation and Main Layout
nav.jobs=Užsakymai
nav.job.create=Užsakymo kūrimas
nav.customers=Klientai
nav.customers=Adres\u0173 knyga
nav.appusers=Programėlės naudotojai
nav.statistics=Statistika
nav.invoices=Sąskaitos faktūros
@@ -31,7 +31,7 @@ profile.lastname=Pavardė
profile.phone=Telefono numeris
profile.fax=Telefonas (faksas)
profile.mobile=Telefonas (mob.)
profile.email=El. pašto adresas (prisijungimas)*
profile.email=El. pašto adresas*
profile.street=Gatvė
profile.housenr=Namo nr.
profile.addressadd=Adreso priedas
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vietovė (atsiėmimas)
addjob.address.city.placeholder.delivery=Vietovė (pristatymas)
addjob.address.delivery.street.placeholder=Gatvė (pristatymas)
addjob.address.delivery.addition.placeholder=Adreso priedas (pristatymas)
addjob.address.save=Išsaugoti adresą
addjob.address.save=Pridėti adresą į adresų knygą
addjob.address.update=Atnaujinti adresą adresų knygoje
addjob.section.pickup=Atsiėmimas
addjob.section.delivery=Pristatymas
addjob.stations.apply=Pritaikyti stotis
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. nuotraukų
addjob.tasks.photo.max=Maks. nuotraukų
addjob.tasks.barcode.min=Min. brūkšninių kodų
addjob.tasks.barcode.max=Maks. brūkšninių kodų
addjob.tasks.signature.noconfig=Konfigūracija nereikalinga
addjob.tasks.signature.notelabel=Pastaba (neprivaloma)
addjob.tasks.signature.notelabel.placeholder=Įveskite patarimo tekstą pastabai
addjob.tasks.todolist.title=Užduočių sąrašas
addjob.tasks.todolist.item.placeholder=Įveskite užduotį
addjob.tasks.todolist.add=Pridėti užduotį

View File

@@ -5,7 +5,7 @@ dialog.confirm=Apstiprināt
# Navigation and Main Layout
nav.jobs=Uzdevumi
nav.job.create=Izveidot uzdevumu
nav.customers=Klienti
nav.customers=Adrešu gr\u0101mata
nav.appusers=Lietotnes lietotāji
nav.statistics=Statistika
nav.invoices=Rēķini
@@ -31,7 +31,7 @@ profile.lastname=Uzvārds
profile.phone=Tālruņa numurs
profile.fax=Tālrunis (fakss)
profile.mobile=Tālrunis (mobilais)
profile.email=E-pasta adrese (pieteikšanās)*
profile.email=E-pasta adrese
profile.street=Iela
profile.housenr=Mājas nr.
profile.addressadd=Adreses papildinājums
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vieta (saņemšana)
addjob.address.city.placeholder.delivery=Vieta (piegāde)
addjob.address.delivery.street.placeholder=Iela (piegāde)
addjob.address.delivery.addition.placeholder=Adreses papildinājums (piegāde)
addjob.address.save=Saglabāt adresi
addjob.address.save=Pievienot adresi adrešu grāmatai
addjob.address.update=Atjaunin\u0101t adresi adrešu gr\u0101mat\u0101
addjob.section.pickup=Saņemšana
addjob.section.delivery=Piegāde
addjob.stations.apply=Pārņemt stacijas
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. fotogrāfijas
addjob.tasks.photo.max=Maks. fotogrāfijas
addjob.tasks.barcode.min=Min. svītrkodi
addjob.tasks.barcode.max=Maks. svītrkodi
addjob.tasks.signature.noconfig=Konfigurācija nav nepieciešama
addjob.tasks.signature.notelabel=Piezīme (neobligāta)
addjob.tasks.signature.notelabel.placeholder=Ievadiet padoma tekstu piezīmei
addjob.tasks.todolist.title=Uzdevumu saraksts
addjob.tasks.todolist.item.placeholder=Ievadiet uzdevumu
addjob.tasks.todolist.add=Pievienot uzdevumu

View File

@@ -5,7 +5,7 @@ dialog.confirm=Potwierd\u017a
# Navigation and Main Layout
nav.jobs=Zlecenia
nav.job.create=Tworzenie zlecenia
nav.customers=Klienci
nav.customers=Ksi\u0105\u017cka adresowa
nav.appusers=U\u017cytkownicy aplikacji
nav.statistics=Statystyki
nav.invoices=Faktury
@@ -31,7 +31,7 @@ profile.lastname=Nazwisko
profile.phone=Numer telefonu
profile.fax=Telefon (faks)
profile.mobile=Telefon (kom\u00f3rkowy)
profile.email=Adres e-mail (login)*
profile.email=Adres e-mail
profile.street=Ulica
profile.housenr=Nr domu
profile.addressadd=Dodatek do adresu
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Miejscowo\u015b\u0107 (odbi\u00f3r)
addjob.address.city.placeholder.delivery=Miejscowo\u015b\u0107 (dostawa)
addjob.address.delivery.street.placeholder=Ulica (dostawa)
addjob.address.delivery.addition.placeholder=Dodatek do adresu (dostawa)
addjob.address.save=Zapisz adres
addjob.address.save=Dodaj adres do ksi\u0105\u017cki adresowej
addjob.address.update=Zaktualizuj adres w ksi\u0105\u017cce adresowej
addjob.section.pickup=Odbi\u00f3r
addjob.section.delivery=Dostawa
addjob.stations.apply=Zastosuj stacje
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. zdj\u0119\u0107
addjob.tasks.photo.max=Maks. zdj\u0119\u0107
addjob.tasks.barcode.min=Min. kod\u00f3w kreskowych
addjob.tasks.barcode.max=Maks. kod\u00f3w kreskowych
addjob.tasks.signature.noconfig=Konfiguracja nie jest wymagana
addjob.tasks.signature.notelabel=Notatka (opcjonalnie)
addjob.tasks.signature.notelabel.placeholder=Wprowadź tekst podpowiedzi dla notatki
addjob.tasks.todolist.title=Lista zada\u0144
addjob.tasks.todolist.item.placeholder=Wprowad\u017a zadanie
addjob.tasks.todolist.add=Dodaj zadanie

View File

@@ -5,7 +5,7 @@ dialog.confirm=Подтвердить
# Navigation and Main Layout
nav.jobs=Заказы
nav.job.create=Создание заказа
nav.customers=Клиенты
nav.customers=Адресная книга
nav.appusers=Пользователи приложения
nav.statistics=Статистика
nav.invoices=Счета
@@ -31,7 +31,7 @@ profile.lastname=Фамилия
profile.phone=Номер телефона
profile.fax=Телефон (факс)
profile.mobile=Телефон (мобильный)
profile.email=Адрес электронной почты (логин)*
profile.email=Адрес электронной почты
profile.street=Улица
profile.housenr=Дом
profile.addressadd=Дополнение к адресу
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Город (забор)
addjob.address.city.placeholder.delivery=Город (доставка)
addjob.address.delivery.street.placeholder=Улица (доставка)
addjob.address.delivery.addition.placeholder=Дополнение к адресу (доставка)
addjob.address.save=Сохранить адрес
addjob.address.save=Добавить адрес в адресную книгу
addjob.address.update=Обновить адрес в адресной книге
addjob.section.pickup=Забор
addjob.section.delivery=Доставка
addjob.stations.apply=Применить станции
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Мин. фото
addjob.tasks.photo.max=Макс. фото
addjob.tasks.barcode.min=Мин. штрих-кодов
addjob.tasks.barcode.max=Макс. штрих-кодов
addjob.tasks.signature.noconfig=Настройка не требуется
addjob.tasks.signature.notelabel=Примечание (необязательно)
addjob.tasks.signature.notelabel.placeholder=Введите текст подсказки для примечания
addjob.tasks.todolist.title=Список дел
addjob.tasks.todolist.item.placeholder=Введите задачу
addjob.tasks.todolist.add=Добавить задачу

View File

@@ -5,7 +5,7 @@ dialog.confirm=Onayla
# Navigation and Main Layout
nav.jobs=\u0130\u015fler
nav.job.create=\u0130\u015f Olu\u015ftur
nav.customers=M\u00fc\u015fteriler
nav.customers=Adres Defteri
nav.appusers=Uygulama Kullan\u0131c\u0131lar\u0131
nav.statistics=\u0130statistikler
nav.invoices=Faturalar
@@ -31,7 +31,7 @@ profile.lastname=Soyad
profile.phone=Telefon Numaras\u0131
profile.fax=Telefon (Faks)
profile.mobile=Telefon (Mobil)
profile.email=E-Posta Adresi (Giri\u015f)*
profile.email=E-Posta Adresi*
profile.street=Sokak
profile.housenr=Kap\u0131 No
profile.addressadd=Adres Eki
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=\u015eehir (Al\u0131m)
addjob.address.city.placeholder.delivery=\u015eehir (Teslimat)
addjob.address.delivery.street.placeholder=Sokak (Teslimat)
addjob.address.delivery.addition.placeholder=Adres eki (Teslimat)
addjob.address.save=Adresi Kaydet
addjob.address.save=Adresi adres defterine ekle
addjob.address.update=Adres defterindeki adresi g\u00fcncelle
addjob.section.pickup=Al\u0131m
addjob.section.delivery=Teslimat
addjob.stations.apply=\u0130stasyonlar\u0131 \u00fcbernehmennehmen
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Foto\u011fraf
addjob.tasks.photo.max=Maks. Foto\u011fraf
addjob.tasks.barcode.min=Min. Barkod
addjob.tasks.barcode.max=Maks. Barkod
addjob.tasks.signature.noconfig=Yap\u0131land\u0131rma gerekli de\u011fil
addjob.tasks.signature.notelabel=Not (iste\u011fe ba\u011fl\u0131)
addjob.tasks.signature.notelabel.placeholder=Not i\u00e7in ipucu metnini girin
addjob.tasks.todolist.title=Yap\u0131lacaklar Listesi
addjob.tasks.todolist.item.placeholder=Yap\u0131lacak \u00f6\u011feyi girin
addjob.tasks.todolist.add=Yap\u0131lacak \u00d6\u011fe Ekle