Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen

Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung.

Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
This commit is contained in:
Sven committed 2026-10-13 20:31:31 +02:00
1 parent 42c87282ee
commit 363bc27c26
190 files changed
+14815 -1857

No files matched your search

@@ -0,0 +1,47 @@
package de.stadtbote.votian;
import de.stadtbote.votian.database.DatabaseService;
import org.springframework.stereotype.Service;
import java.util.*;
import java.time.*;
import java.time.format.*;
import static de.stadtbote.votian.AdminSupport.*;
import static de.stadtbote.votian.LegacyMapping.*;
import static de.stadtbote.votian.OrderEntryService.fields;
import static de.stadtbote.votian.CrmScope.marks;
/** Category 15, matching the PHP Bewertung form and its persisted value formats. */
@Service
class EvaluationService extends MetafieldRecords {
static final DateTimeFormatter DATE=DateTimeFormatter.ofPattern("dd.MM.uuuu").withResolverStyle(ResolverStyle.STRICT);
EvaluationService(DatabaseService db,FormAdministration forms,MessageAdministration messages){super(db,forms,messages,15);}
void access(LegacyUser u){u.require(0);u.require(8);}
Map<String,Object> options(LegacyUser u){return fields("headquarters",headquarters(u),"currentHeadquarters",u.headquartersId());}
List<Map<String,Object>> lookup(LegacyUser u,long hq,String kind,String q){checked(u,hq);if(q==null)q="";if(q.length()>100)throw bad("Suchbegriff ist zu lang.");String search="%"+q.replace("\\","\\\\").replace("%","\\%").replace("_","\\_")+"%";
return switch(kind){
case "customer" -> db.queryForList("SELECT cs.cs_id AS id,cs.cs_eid AS value,CONCAT_WS(' · ',cs.cs_eid,cmp.cmp_comp,cmp.cmp_comp2) AS label FROM customer cs JOIN company cmp ON cmp.cmp_id=cs.cmp_id WHERE cs.hq_id=? AND (cs.cs_eid LIKE ? OR cmp.cmp_comp LIKE ?) ORDER BY cs.cs_eid LIMIT 100",hq,search,search);
case "courier" -> {u.require(1);yield db.queryForList("SELECT cr.cr_id AS id,cr.cr_eid AS value,CONCAT_WS(' · ',cr.cr_eid,cmp.cmp_comp) AS label FROM courier cr JOIN company cmp ON cmp.cmp_id=cr.cmp_id WHERE cr.hq_id=? AND (cr.cr_eid LIKE ? OR cmp.cmp_comp LIKE ?) ORDER BY cr.cr_eid LIMIT 100",hq,search,search);}
case "vehicle" -> {u.require(1);yield db.queryForList("SELECT cv.crvh_id AS id,cv.crvh_sid AS value,cv.crvh_sid AS label FROM couriervehicle cv JOIN courier cr ON cr.cr_id=cv.cr_id WHERE cr.hq_id=? AND cv.crvh_sid LIKE ? ORDER BY cv.crvh_sid LIMIT 100",hq,search);}
case "vehicleType" -> db.queryForList("SELECT mt_sort AS id,mt_sort AS value,mt_value AS label FROM metatype WHERE mt_type='vehicletype' ORDER BY mt_sort");
case "group" -> {var hqs=headquarters(u).stream().map(r->id(r,"id")).toList();var args=new ArrayList<Object>(List.of(mandator(db,u)));args.addAll(hqs);args.add(search);yield db.queryForList("SELECT grp_id AS id,grp_id AS value,grp_name AS label FROM groups WHERE md_id=? AND (hq_id=0 OR hq_id IN ("+marks(hqs.size())+")) AND grp_name LIKE ? ORDER BY grp_name LIMIT 100",args.toArray());}
case "branch" -> db.queryForList("SELECT br_id AS id,br_id AS value,br_name AS label FROM branch WHERE br_name LIKE ? ORDER BY br_name LIMIT 100",search);
default -> throw bad("Unbekannte Bewertungsauswahl.");
};
}
String validateValue(LegacyUser u,long hq,Map<String,Object> def,String value){String type=text(def,"type"),v=value.strip();if(v.isEmpty())return "";switch(type){
case "date_begin","date_end","date" -> {parseDate(v);return v;}
case "checkboxes_hq" -> {if(!v.matches("[0-9]+(,[0-9]+)*"))throw bad("Ungültige Niederlassungsauswahl.");var allowed=headquarters(u).stream().map(r->Long.toString(id(r,"id"))).toList();var requested=Arrays.stream(v.split(",")).distinct().toList();if(!allowed.containsAll(requested))throw bad("Niederlassung nicht freigegeben.");return String.join(",",requested);}
case "button:cs:cs_eid" -> {one(db.queryForList("SELECT cs_id FROM customer WHERE hq_id=? AND cs_eid=?",hq,v));return v;}
case "button:cr:cr_eid" -> {u.require(1);one(db.queryForList("SELECT cr_id FROM courier WHERE hq_id=? AND cr_eid=?",hq,v));return v;}
case "select:crvh:crvh_sid" -> {u.require(1);if(db.queryForList("SELECT cv.crvh_id FROM couriervehicle cv JOIN courier cr ON cr.cr_id=cv.cr_id WHERE cr.hq_id=? AND cv.crvh_sid=?",hq,v).isEmpty())throw bad("Fahrzeug nicht gefunden.");return v;}
case "select:vht" -> {one(db.queryForList("SELECT mt_sort FROM metatype WHERE mt_type='vehicletype' AND mt_sort=?",number(v)));return v;}
case "select:grp" -> {var hqs=headquarters(u).stream().map(r->id(r,"id")).toList();var args=new ArrayList<Object>(List.of(number(v),mandator(db,u)));args.addAll(hqs);one(db.queryForList("SELECT grp_id FROM groups WHERE grp_id=? AND md_id=? AND (hq_id=0 OR hq_id IN ("+marks(hqs.size())+"))",args.toArray()));return v;}
case "select:br" -> {one(db.queryForList("SELECT br_id FROM branch WHERE br_id=?",number(v)));return v;}
default -> throw bad(text(def,"label")+": Nicht unterstützter Bewertungsfeldtyp.");
}}
static long number(String v){try{long n=Long.parseLong(v);if(n<0)throw new NumberFormatException();return n;}catch(NumberFormatException e){throw bad("Ungültiger Auswahlwert.");}}
static LocalDate parseDate(String v){try{return LocalDate.parse(v,DATE);}catch(DateTimeParseException e){throw bad("Ungültiges Datum (TT.MM.JJJJ).");}}
void validateValues(List<Map<String,Object>> defs,Map<Long,String> values){LocalDate begin=null,end=null;for(var d:defs){String v=values.get(id(d,"id"));if(v==null||v.isBlank())continue;if(text(d,"type").equals("date_begin"))begin=parseDate(v);if(text(d,"type").equals("date_end"))end=parseDate(v);}if(begin!=null&&end!=null&&end.isBefore(begin))throw bad("Das Enddatum liegt vor dem Beginn.");}
@Override Map<String,Object> rendering(LegacyUser u,long hq,long object){var result=super.rendering(u,hq,object);var data=new LinkedHashMap<>((Map<String,Object>)result.get("data"));var values=new LinkedHashMap<>((Map<Long,String>)data.get("values"));var defs=(List<Map<String,Object>>)result.get("definitions");for(var def:defs){long field=id(def,"id");String type=text(def,"type"),v=values.getOrDefault(field,"");if(v.isBlank())continue;if(type.equals("checkboxes_hq")){var names=headquarters(u).stream().collect(java.util.stream.Collectors.toMap(r->Long.toString(id(r,"id")),r->text(r,"label")));values.put(field,Arrays.stream(v.split(",")).map(x->names.getOrDefault(x,x)).collect(java.util.stream.Collectors.joining(", ")));}}
data.put("values",values);result.put("data",data);return result;
}
}