Ergänze Auftragsbearbeitung, Disposition, Stammdaten, Kunden- und Transporteurportale, Lagerverwaltung, Abrechnung, Exporte und Statistik. Füge Kontoeinstellungen, Mitteilungen, Automatikdienste und Betriebsstatus sowie die zugehörigen Tests und PHP-Abgleichsdokumentation hinzu. Vergrößere die persönlichen Auftragslisten auf einheitliche Höhen und richte Statusanzeigen sowie die Ranking-Vorschau-Steuerelemente bündig aus. Validierung: 44 Frontendtests und Produktionsbuild erfolgreich. Backend funktional unverändert gegenüber dem vollständigen Lauf mit 1.624 erfolgreichen und drei übersprungenen Tests. Layout im Browser und lokale Bereitstellung geprüft.
84 lines
11 KiB
Java
84 lines
11 KiB
Java
package de.stadtbote.votian;
|
|
|
|
import de.stadtbote.votian.database.DatabaseService;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.web.bind.annotation.*;
|
|
import java.time.*;
|
|
import java.util.*;
|
|
import static de.stadtbote.votian.AdminSupport.*;
|
|
import static de.stadtbote.votian.LegacyMapping.*;
|
|
import static de.stadtbote.votian.OrderEntryService.fields;
|
|
|
|
/** customer/jb_list.php: a customer may dispatch only to its own favoured couriers. */
|
|
@RestController
|
|
@RequestMapping("/api/customer-portal/orders/{job}/courier-assignment")
|
|
class CustomerCourierAssignment {
|
|
record Selection(long vehicle,long courier,boolean resetPreviousAvailableTime){}
|
|
record Command(String requestId,String revision,Selection selection,boolean confirmed){}
|
|
record State(long customer,String accessRevision,Map<String,Object> job,List<Map<String,Object>> stops,List<Map<String,Object>> vehicles,List<String> blockers){}
|
|
record Prepared(State state,Selection selection,Map<String,Object> vehicle,boolean loseRanking,String rankingAfter,List<String> warnings,String revision){}
|
|
final DatabaseService db;final BusinessSupport support;final OrderEntryCatalog catalog;
|
|
CustomerCourierAssignment(DatabaseService db,BusinessSupport support){this.db=db;this.support=support;catalog=new OrderEntryCatalog(db);}
|
|
@GetMapping Object get(Authentication a,@PathVariable long job){return options(ApiController.user(a),job);}
|
|
@PostMapping("/preview") Object preview(Authentication a,@PathVariable long job,@RequestBody Selection s){return preview(ApiController.user(a),job,s);}
|
|
@PostMapping Object post(Authentication a,@PathVariable long job,@RequestBody Command c){return save(ApiController.user(a),job,c);}
|
|
|
|
State state(LegacyUser u,long job){
|
|
var context=CustomerPortalScope.context(db,u);context.require(4);long customer=id(context.account(),"cs_id");
|
|
var own=CustomerPortalScope.customer(context,customer);var args=new ArrayList<Object>(new TreeSet<>(own.allowed()));args.add(job);args.add(u.headquartersId());
|
|
var portal=new CustomerPortal(db);String related=catalog.parameter(0,"GLOBAL_USE_RELATED_CUSTOMER"),enabled=catalog.parameter(0,"CUSTOMER_MASK_JOBLIST_DISPO_FAV_ENABLED_"+customer);
|
|
if(!enabled.equals("1")||related.equals("1"))throw CustomerPortalScope.denied();
|
|
var rows=db.queryForList("SELECT jb.* FROM job jb WHERE ("+portal.payer(u)+") IN ("+CourierPortal.placeholders(own.allowed().size())+") AND jb.jb_id=? AND jb.hq_id=?",args.toArray());
|
|
if(rows.size()!=1||id(rows.getFirst(),"jb_id_parent")>0)throw CustomerPortalScope.denied();var j=rows.getFirst();var blockers=new ArrayList<String>();
|
|
if(!Set.of(0L,1L,8L,9L).contains(id(j,"jb_status"))||id(j,"jb_storno")!=0)blockers.add("Nur offene, nicht stornierte Aufträge können zugewiesen werden.");
|
|
if(OrderOperations.exported(j)||!db.queryForList("SELECT jb_id FROM invoice WHERE jb_id=?",job).isEmpty()||!db.queryForList("SELECT gdc_id FROM genericdatacontainer WHERE gdc_obj_type='jb' AND gdc_obj_id=? AND gdc_gen_fieldname='ng_invoice_lock'",job).isEmpty())blockers.add("Der Auftrag ist exportiert oder einer Rechnung zugeordnet.");
|
|
if(!db.queryForList("SELECT jb_id FROM job WHERE jb_id_copy=? AND jb_storno IN (3,4)",job).isEmpty())blockers.add("Für den Auftrag besteht bereits eine Gegenbuchung.");
|
|
var lock=dateTime(j,"jb_locktime");if(lock!=null&&lock.isAfter(db.queryForObject("SELECT NOW()",java.sql.Timestamp.class).toLocalDateTime().minusMinutes(5)))blockers.add("Der Auftrag wird gerade in der alten Anwendung bearbeitet.");
|
|
if(id(j,"hq_id_dispo")>0&&id(j,"hq_id_dispo")!=u.headquartersId())blockers.add("Die Zuweisung erfolgt durch eine andere Niederlassung.");
|
|
var stops=db.queryForList("SELECT t.tr_id,t.tr_sort,stopaddr.ad_zipcode FROM tour t LEFT JOIN address stopaddr ON stopaddr.ad_id=t.ad_id WHERE t.jb_id=? ORDER BY t.tr_sort,t.tr_id",job);
|
|
if(stops.isEmpty()||text(stops.getFirst(),"ad_zipcode").isBlank())blockers.add("Die Startadresse ist unvollständig.");
|
|
boolean multiple=AssignmentVehicles.multiple(catalog,u);
|
|
String sql="SELECT cv.crvh_id,cv.crvh_sid,cv.vht_id,cr.cr_sid,cr.cr_available,cr.cr_occupied,cr.cr_locationzipcode,cr.cr_availabletime,"+AssignmentVehicles.IDENTITY+" "+AssignmentVehicles.from(false,multiple)+" AND cr.cr_sid=cv.crvh_sid AND EXISTS (SELECT 1 FROM customercourier fav WHERE fav.cs_id=? AND fav.cr_id=cr.cr_id AND fav.cscr_relation=1) AND NOT EXISTS (SELECT 1 FROM customercourier blocked WHERE blocked.cs_id=? AND blocked.cr_id=cr.cr_id AND blocked.cscr_relation<>1) ORDER BY cv.crvh_sid,cv.crvh_id LIMIT 1001";
|
|
var vehicles=AssignmentVehicles.identities(db.queryForList(sql,u.headquartersId(),customer,customer)).stream().filter(v->!AssignmentVehicles.ambiguous(v,multiple)&&id(v,"logged_drivers")==1).toList();
|
|
if(vehicles.size()>1000)throw bad("Zu viele bevorzugte Fahrzeuge. Bitte die Zuordnungen prüfen.");
|
|
String access=revision(List.of(customer,own.allowed(),context.rights(),enabled,related,multiple));
|
|
return new State(customer,access,j,stops,vehicles,List.copyOf(blockers));
|
|
}
|
|
Map<String,Object> options(LegacyUser u,long job){return db.readTransaction(()->{var s=state(u,job);return fields("id",job,"current",text(s.job(),"cr_sid"),"canResetPrevious",id(s.job(),"cr_id")>0,"blockers",s.blockers(),"vehicles",s.vehicles().stream().map(v->fields("id",id(v,"crvh_id"),"courier",id(v,"cr_id"),"sid",text(v,"crvh_sid"),"available",id(v,"cr_available")==1,"occupied",id(v,"cr_occupied")!=0)).toList());});}
|
|
Prepared prepare(LegacyUser u,long job,Selection selection){
|
|
if(selection==null||selection.vehicle()<=0||selection.courier()<=0)throw bad("Bitte einen bevorzugten Kurier wählen.");
|
|
var s=state(u,job);if(!s.blockers().isEmpty())throw bad(String.join(" · ",s.blockers()));
|
|
var matches=s.vehicles().stream().filter(v->id(v,"crvh_id")==selection.vehicle()&&id(v,"cr_id")==selection.courier()).toList();
|
|
if(matches.size()!=1)throw CustomerPortalScope.denied();var v=matches.getFirst();var j=s.job();
|
|
if(id(j,"cr_id_order")==selection.courier()||id(j,"cr_id")==selection.courier())throw bad("Dieser Kurier ist dem Auftrag bereits zugewiesen.");
|
|
if(selection.resetPreviousAvailableTime()&&(id(j,"cr_id")<=0||db.queryForList("SELECT cr_id FROM courier WHERE cr_id=? AND hq_id=?",id(j,"cr_id"),u.headquartersId()).size()!=1))throw bad("Es gibt keinen bisherigen Kurier mit zurücksetzbarer Freimeldezeit.");
|
|
var warnings=new ArrayList<String>();if(id(v,"cr_available")!=1)warnings.add("Der Kurier ist nicht als verfügbar gemeldet.");if(id(v,"cr_occupied")!=0)warnings.add("Der Kurier ist bereits als belegt gemeldet.");if(id(v,"vht_id")!=id(j,"vht_id"))warnings.add("Die Fahrzeugklasse weicht vom Auftrag ab.");
|
|
var evidence=new ArrayList<Object>();evidence.add(s);evidence.add(selection);String start=text(s.stops().getFirst(),"ad_zipcode");long hq=id(j,"hq_id_dispo")>0?id(j,"hq_id_dispo"):u.headquartersId();
|
|
var lead=db.queryForList("SELECT p.srvp_id,lt.srvpt_traveltime FROM serviceplz p JOIN serviceplztraveltime lt ON lt.srvp_id=p.srvp_id WHERE p.srvp_plz=? AND lt.hq_id=? ORDER BY p.srvp_id",start,hq);evidence.add(lead);
|
|
if(lead.size()>1)throw bad("Die Anfahrtszeit für den Rankingverlust ist mehrdeutig.");
|
|
String after="";boolean deferred=false;var time=dateTime(j,"jb_ordertime");
|
|
if(time!=null&&lead.size()==1&&lead.getFirst().get("srvpt_traveltime")!=null){var threshold=time.minusMinutes(Math.max(30,id(lead.getFirst(),"srvpt_traveltime")));after=threshold.toString();deferred=!db.queryForObject("SELECT NOW()",java.sql.Timestamp.class).toLocalDateTime().isAfter(threshold);}
|
|
boolean lose=!deferred&&new FleetRanking(db,catalog).area(u,text(v,"cr_locationzipcode"),start,evidence).contains();
|
|
if(deferred)warnings.add("Künftiger Auftrag: Rankingverlust wird erst nach "+after.replace('T',' ')+" geprüft.");
|
|
evidence.add(after);evidence.add(deferred);evidence.add(lose);return new Prepared(s,selection,v,lose,after,List.copyOf(warnings),revision(evidence));
|
|
}
|
|
Map<String,Object> preview(LegacyUser u,long job,Selection selection){return db.readTransaction(()->{var p=prepare(u,job,selection);return fields("id",job,"revision",p.revision(),"sid",text(p.vehicle(),"crvh_sid"),"previous",text(p.state().job(),"cr_sid"),"loseRanking",p.loseRanking(),"rankingAfter",p.rankingAfter(),"warnings",p.warnings(),"resetPreviousAvailableTime",selection.resetPreviousAvailableTime());});}
|
|
Map<String,Object> save(LegacyUser u,long job,Command c){
|
|
if(c==null||!c.confirmed()||c.selection()==null||c.revision()==null||!c.revision().matches("[a-f0-9]{64}"))throw bad("Bitte die Zuweisung zuerst prüfen und bestätigen.");BusinessSupport.token(c.requestId());
|
|
support.transactional("job","courier","autoranking","genericdatacontainer","phoenix_pda.commandexec");
|
|
return db.customerAssignmentWriteTransaction(()->{
|
|
state(u,job);Object intent=List.of("CUSTOMER_ASSIGN",job,c);Long old=support.receipt(u,c.requestId(),intent);if(old!=null)return fields("id",job,"replayed",true);
|
|
var p=prepare(u,job,c.selection());if(!p.revision().equals(c.revision()))throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT,"Auftrag, Fahrer oder Freigabe wurden geändert. Bitte erneut prüfen.");
|
|
var j=p.state().job();long next=c.selection().courier();var now=LocalDateTime.now().withNano(0);
|
|
for(long previous:new TreeSet<>(List.of(id(j,"cr_id"),id(j,"cr_id_order"))))if(previous>0&&previous!=next)db.insert("`phoenix_pda`.commandexec",fields("cr_id",previous,"hq_id",u.headquartersId(),"cmd_command","4","cmde_mode","1","cmde_param",Long.toString(job),"cmde_exec_time",now.plusSeconds(30),"cmde_exec_timelimit",now.plusHours(36)));
|
|
if(c.selection().resetPreviousAvailableTime())db.update("UPDATE courier SET cr_availabletime=? WHERE cr_id=? AND hq_id=?",now,id(j,"cr_id"),u.headquartersId());
|
|
var ranks=db.queryForList("SELECT cr_id FROM autoranking WHERE jb_id=? AND cr_id=?",job,next);if(ranks.size()>1)throw bad("Die bisherige Kurierzuweisung ist mehrdeutig.");
|
|
if(ranks.isEmpty())BusinessSupport.insertWithoutKey(db,"autoranking",fields("jb_id",job,"cr_id",next,"ar_challenge",0,"ar_lastassigntime",now,"ar_looseranking",p.loseRanking()?1:0,"ar_locating",0));
|
|
else db.update("UPDATE autoranking SET ar_challenge=0,ar_lastassigntime=?,ar_looseranking=?,ar_locating=0 WHERE jb_id=? AND cr_id=?",now,p.loseRanking()?1:0,job,next);
|
|
var changes=fields("cr_id",0,"cr_id_order",next,"cr_sid",text(p.vehicle(),"crvh_sid"),"jb_status",0,"jb_globaljob",0,"jb_autoranking",0,"jb_taketime",null,"vht_id_real",id(p.vehicle(),"vht_id"),"jb_version",id(j,"jb_version")+1);BusinessSupport.update(db,"job","jb_id",job,changes);
|
|
var assigned=new LinkedHashMap<>(j);assigned.putAll(changes);new OrderSurvey(db,catalog).apply(u,assigned,c.requestId(),support);
|
|
support.audit(u,job,p.state().customer(),next,7,"Kundenzuweisung · LOST_RANKING="+(p.loseRanking()?"YES":"NO"),c.requestId());support.receipt(u,c.requestId(),intent,job);return fields("id",job,"replayed",false);
|
|
});
|
|
}
|
|
}
|