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:
1 parent
42c87282ee
commit
363bc27c26
190 files changed
+14815
-1857
No files matched your search
@@ -0,0 +1,264 @@
|
||||
package de.stadtbote.votian;
|
||||
|
||||
import de.stadtbote.votian.database.DatabaseService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.*;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import static de.stadtbote.votian.AdminSupport.*;
|
||||
import static de.stadtbote.votian.LegacyMapping.*;
|
||||
import static de.stadtbote.votian.OrderEntryService.fields;
|
||||
|
||||
/** Port of groupware/appointment.php. Notifications use the separate preview/send workflow. */
|
||||
@Service
|
||||
public class AppointmentService {
|
||||
static final String TABLE="`phoenix_group`.appointment";
|
||||
static final List<String> COLUMNS=List.of("ap_id","hq_id","usr_id","ap_type","ap_category_1","ap_category_2","ap_category_3","ap_category_4","ap_participants","ap_text","ap_execdate","ap_enddate","ap_confirmed","ap_modifytime","cs_id","csc_id","pt_id");
|
||||
private final DatabaseService db;
|
||||
private final OrderEntryCatalog catalog;
|
||||
public AppointmentService(DatabaseService db,OrderEntryCatalog catalog) { this.db=db;this.catalog=catalog; }
|
||||
enum Visibility { OWN_PRIVATE,OWN_PARTICIPANTS,OWN_PUBLIC,INVITED,OTHER_PUBLIC }
|
||||
record Query(LocalDate from,LocalDate to,List<Long> headquarters,Long personId,Long customerId,Integer status,Integer kind,Integer monitoring,List<Visibility> visibility,String search) {}
|
||||
record Save(String revision,LocalDateTime startsAt,LocalDateTime endsAt,String text,int status,int visibility,int kind,int monitoring,Long customerId,List<Long> participants) {}
|
||||
record Resubmission(String revision,LocalDateTime startsAt,LocalDateTime endsAt) {}
|
||||
record Sql(String where,Object[] args) {}
|
||||
private void access(LegacyUser user) { user.require(0);user.require(11); }
|
||||
private static String marks(int size) { return String.join(",",Collections.nCopies(size,"?")); }
|
||||
private static String participant(long userId) { return "%,"+userId+",%"; }
|
||||
private List<Map<String,Object>> headquarters(LegacyUser user) {
|
||||
return db.queryForList("SELECT DISTINCT h.hq_id AS id,h.hq_mnemonic AS label FROM headquarters h JOIN mandatorheadquarters m ON m.hq_id=h.hq_id JOIN mandatorheadquarters own ON own.md_id=m.md_id WHERE own.hq_id=? AND own.md_id>0"+(user.hasRight(10)?"":" AND h.hq_id=?")+" ORDER BY h.hq_mnemonic",user.hasRight(10)?new Object[]{user.headquartersId()}:new Object[]{user.headquartersId(),user.headquartersId()});
|
||||
}
|
||||
private List<Long> scope(LegacyUser user,List<Long> requested) {
|
||||
if(requested==null||requested.isEmpty()||requested.size()>100||requested.stream().anyMatch(Objects::isNull))throw bad("Bitte mindestens eine Niederlassung wählen.");
|
||||
var ids=requested.stream().distinct().sorted().toList();
|
||||
if(ids.size()==1&&ids.getFirst()==user.headquartersId())return ids;
|
||||
user.require(10);
|
||||
var allowed=headquarters(user).stream().map(r->id(r,"id")).toList();
|
||||
if(!allowed.containsAll(ids))throw new AccessDeniedException("Niederlassung ist für diesen Mandanten nicht freigegeben.");
|
||||
return ids;
|
||||
}
|
||||
private List<Long> allowedHeadquarters(LegacyUser user) {
|
||||
var ids=headquarters(user).stream().map(r->id(r,"id")).toList();
|
||||
if(ids.isEmpty())throw new AccessDeniedException("Keine Niederlassung freigegeben.");return ids;
|
||||
}
|
||||
private List<Map<String,Object>> employees(List<Long> hqs) {
|
||||
var rows=db.queryForList("SELECT u.usr_id AS id,CONCAT_WS(' ',u.usr_firstname,u.usr_name) AS label,u.hq_id AS headquartersId,h.hq_mnemonic AS headquarters,e.emp_group AS groups FROM user u JOIN employee e ON e.usr_id=u.usr_id JOIN headquarters h ON h.hq_id=u.hq_id WHERE u.usr_type=1 AND u.hq_id IN ("+marks(hqs.size())+") ORDER BY u.usr_name,u.usr_firstname,u.usr_id LIMIT 5001",hqs.toArray());
|
||||
if(rows.size()>5000)throw bad("Zu viele Mitarbeiter. Bitte die Niederlassungsauswahl eingrenzen.");
|
||||
return rows;
|
||||
}
|
||||
private Map<String,List<Map<String,Object>>> categories() {
|
||||
var result=new LinkedHashMap<String,List<Map<String,Object>>>();
|
||||
var labels=new HashMap<String,String>();
|
||||
for(var p:db.queryForList("SELECT par_key,par_value FROM parameter WHERE emp_id=0 AND hq_id=0 AND par_key LIKE 'MASK_AP_CAT_%'"))labels.put(text(p,"par_key"),text(p,"par_value"));
|
||||
for(int category=1;category<=4;category++) {
|
||||
var options=new ArrayList<Map<String,Object>>();
|
||||
for(var r:db.queryForList("SELECT mt_sort AS id,MIN(mt_value) AS label FROM metatype WHERE mt_type=? GROUP BY mt_sort ORDER BY mt_sort","appointment_category_"+category)) {
|
||||
long id=id(r,"id");String override=labels.get("MASK_AP_CAT_"+category+"_"+id);
|
||||
options.add(fields("id",id,"label",override==null||override.isBlank()?text(r,"label"):override));
|
||||
}
|
||||
if(category==4&&options.stream().noneMatch(o->id(o,"id")==2))options.add(fields("id",2L,"label",labels.getOrDefault("MASK_AP_CAT_4_9","Erledigt")));
|
||||
result.put("category"+category,options);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public Map<String,Object> options(LegacyUser user) {
|
||||
access(user);var hqs=headquarters(user);if(hqs.isEmpty())throw new AccessDeniedException("Keine Niederlassung freigegeben.");
|
||||
var people=employees(hqs.stream().map(r->id(r,"id")).toList());
|
||||
var groups=db.queryForList("SELECT grp_id AS id,grp_name AS label FROM groups WHERE md_id=? ORDER BY grp_name",mandator(db,user));
|
||||
var groupViews=new ArrayList<Map<String,Object>>();
|
||||
for(var g:groups) {
|
||||
var members=people.stream().filter(p->GroupAdministration.memberships(text(p,"groups")).contains(id(g,"id"))).map(p->id(p,"id")).toList();
|
||||
if(!members.isEmpty())groupViews.add(fields("id",id(g,"id"),"label",text(g,"label"),"members",members));
|
||||
}
|
||||
return fields("headquarters",hqs,"currentHeadquarters",user.headquartersId(),"currentUser",user.userId(),"now",LocalDateTime.now(ZoneId.of("Europe/Berlin")).withNano(0),"categories",categories(),"people",people,"groups",groupViews,
|
||||
"defaultMonitoring",catalog.enabled(0,"MASK_AP_CATEGORY_4_PRESET")?1:0,"miscType",catalog.parameter(0,"MASK_AP_CATEGORY_3_MISC_ITEM"),"canReadReports",user.hasRight(16));
|
||||
}
|
||||
public List<Map<String,Object>> customers(LegacyUser user,String q) {
|
||||
access(user);if(q==null||q.length()>100)throw bad("Suchtext zu lang.");var hqs=allowedHeadquarters(user);
|
||||
var args=new ArrayList<Object>(hqs);args.add("%"+q.strip()+"%");
|
||||
return db.queryForList("SELECT c.cs_id AS id,CONCAT_WS(' ',c.cs_eid,co.cmp_comp,co.cmp_comp2) AS label FROM customer c LEFT JOIN company co ON co.cmp_id=c.cmp_id WHERE c.hq_id IN ("+marks(hqs.size())+") AND CONCAT_WS(' ',c.cs_eid,co.cmp_comp,co.cmp_comp2) LIKE ? ORDER BY label,id LIMIT 50",args.toArray());
|
||||
}
|
||||
private void customer(LegacyUser user,long customerId) {
|
||||
var hqs=allowedHeadquarters(user);var args=new ArrayList<Object>(List.of(customerId));args.addAll(hqs);
|
||||
one(db.queryForList("SELECT cs_id FROM customer WHERE cs_id=? AND hq_id IN ("+marks(hqs.size())+")",args.toArray()));
|
||||
}
|
||||
static void validate(Query q,boolean statistics) {
|
||||
if(q==null)throw bad("Terminfilter fehlen.");range(q.from(),q.to());
|
||||
if(q.from().getYear()<1000||q.to().getYear()>9998||!statistics&&ChronoUnit.DAYS.between(q.from(),q.to())>365)throw bad("Kalenderabfragen dürfen höchstens ein Jahr umfassen.");
|
||||
if(q.visibility()==null||q.visibility().isEmpty()||q.visibility().stream().anyMatch(Objects::isNull)||q.visibility().size()>5)throw bad("Bitte mindestens eine Sichtbarkeitsgruppe wählen.");
|
||||
if(q.search()==null||q.search().length()>200)throw bad("Suchtext darf höchstens 200 Zeichen enthalten.");
|
||||
for(Long id:Arrays.asList(q.personId(),q.customerId()))if(id!=null&&id<=0)throw bad("Ungültiger Personen-/Kundenfilter.");
|
||||
for(Integer code:Arrays.asList(q.status(),q.kind(),q.monitoring()))if(code!=null&&(code<0||code>127))throw bad("Ungültiger Kategoriefilter.");
|
||||
}
|
||||
/** Visibility is authorization, not merely a UI search option. No master bypass for private appointments. */
|
||||
private static String visible(LegacyUser user,List<Object> args) {
|
||||
args.add(user.userId());args.add(participant(user.userId()));
|
||||
return "(a.usr_id=? OR a.ap_category_2=3 OR (a.ap_category_2=1 AND a.ap_participants LIKE ?))";
|
||||
}
|
||||
private Sql selection(LegacyUser user,Query q,boolean statistics) {
|
||||
validate(q,statistics);var hqs=scope(user,q.headquarters());
|
||||
var args=new ArrayList<Object>(hqs);
|
||||
String where="a.hq_id IN ("+marks(hqs.size())+") AND "+visible(user,args)+" AND a.ap_execdate<? AND (a.ap_execdate>=? OR a.ap_enddate>=?)";
|
||||
args.add(q.to().plusDays(1).atStartOfDay());args.add(q.from().atStartOfDay());args.add(q.from().atStartOfDay());
|
||||
if(q.personId()!=null){where+=" AND (a.usr_id=? OR a.ap_participants LIKE ?)";args.add(q.personId());args.add(participant(q.personId()));}
|
||||
if(q.customerId()!=null){customer(user,q.customerId());where+=" AND a.cs_id=?";args.add(q.customerId());}
|
||||
Integer[] codes={q.status(),q.kind(),q.monitoring()};int[] categories={1,3,4};
|
||||
for(int i=0;i<codes.length;i++)if(codes[i]!=null){where+=" AND a.ap_category_"+categories[i]+"=?";args.add(codes[i]);}
|
||||
if(!q.search().isBlank()){where+=" AND a.ap_text LIKE ?";args.add("%"+q.search().strip()+"%");}
|
||||
if(!new HashSet<>(q.visibility()).containsAll(List.of(Visibility.values()))) {
|
||||
var clauses=new ArrayList<String>();
|
||||
for(var v:new LinkedHashSet<>(q.visibility()))switch(v) {
|
||||
case OWN_PRIVATE,OWN_PARTICIPANTS,OWN_PUBLIC -> { clauses.add("(a.usr_id=? AND a.ap_category_2=?)");args.add(user.userId());args.add(v==Visibility.OWN_PRIVATE?2:v==Visibility.OWN_PARTICIPANTS?1:3); }
|
||||
case INVITED -> { clauses.add("(a.usr_id<>? AND a.ap_participants LIKE ?)");args.add(user.userId());args.add(participant(user.userId())); }
|
||||
case OTHER_PUBLIC -> { clauses.add("(a.usr_id<>? AND a.ap_category_2=3 AND (a.ap_participants IS NULL OR a.ap_participants NOT LIKE ?))");args.add(user.userId());args.add(participant(user.userId())); }
|
||||
}
|
||||
where+=" AND ("+String.join(" OR ",clauses)+")";
|
||||
}
|
||||
return new Sql(where,args.toArray());
|
||||
}
|
||||
private static String selectColumns() { return String.join(",",COLUMNS.stream().map(c->"a."+c).toList()); }
|
||||
private static String joins() { return " FROM "+TABLE+" a LEFT JOIN user u ON u.usr_id=a.usr_id LEFT JOIN headquarters h ON h.hq_id=a.hq_id LEFT JOIN customer c ON c.cs_id=a.cs_id LEFT JOIN company co ON co.cmp_id=c.cmp_id"; }
|
||||
public Map<String,Object> list(LegacyUser user,Query query) {
|
||||
access(user);return db.readTransaction(()->{
|
||||
var sql=selection(user,query,false);
|
||||
var rows=db.queryForList("SELECT "+selectColumns()+",CONCAT_WS(' ',u.usr_firstname,u.usr_name) AS author,h.hq_mnemonic AS headquarters,CONCAT_WS(' ',c.cs_eid,co.cmp_comp,co.cmp_comp2) AS customer_label"+joins()+" WHERE "+sql.where()+" ORDER BY a.ap_execdate,a.ap_id LIMIT 5001",sql.args());
|
||||
if(rows.size()>5000)throw bad("Mehr als 5000 Termine. Bitte Zeitraum oder Filter eingrenzen.");
|
||||
var names=participantNames(user,rows);
|
||||
return fields("items",rows.stream().map(r->view(user,r,names)).toList(),"total",rows.size(),"from",query.from(),"to",query.to());
|
||||
});
|
||||
}
|
||||
private Map<Long,String> participantNames(LegacyUser user,List<Map<String,Object>> rows) {
|
||||
var ids=new LinkedHashSet<Long>();for(var row:rows)ids.addAll(GroupAdministration.memberships(text(row,"ap_participants")));
|
||||
var names=new HashMap<Long,String>();if(ids.isEmpty())return names;
|
||||
if(ids.size()>10000)throw bad("Zu viele Teilnehmer. Bitte den Zeitraum eingrenzen.");
|
||||
var all=new ArrayList<>(ids);
|
||||
for(int i=0;i<all.size();i+=500) {
|
||||
var batch=all.subList(i,Math.min(i+500,all.size()));var args=new ArrayList<Object>(batch);args.add(user.headquartersId());
|
||||
for(var r:db.queryForList("SELECT DISTINCT u.usr_id AS id,CONCAT_WS(' ',u.usr_firstname,u.usr_name) AS label FROM user u JOIN mandatorheadquarters m ON m.hq_id=u.hq_id JOIN mandatorheadquarters own ON own.md_id=m.md_id WHERE u.usr_id IN ("+marks(batch.size())+") AND own.hq_id=? AND own.md_id>0",args.toArray()))names.put(id(r,"id"),text(r,"label"));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
static Map<String,Object> version(Map<String,Object> row) { var result=new LinkedHashMap<String,Object>();for(String key:COLUMNS)result.put(key,value(row,key));return result; }
|
||||
private static boolean canEdit(LegacyUser user,Map<String,Object> row) { return id(row,"usr_id")==user.userId()||user.hasRight(3)||GroupAdministration.memberships(text(row,"ap_participants")).contains(user.userId()); }
|
||||
private Map<String,Object> view(LegacyUser user,Map<String,Object> row,Map<Long,String> names) {
|
||||
var people=GroupAdministration.memberships(text(row,"ap_participants"));var confirmed=GroupAdministration.memberships(text(row,"ap_confirmed"));
|
||||
return fields("id",id(row,"ap_id"),"headquartersId",id(row,"hq_id"),"headquarters",text(row,"headquarters"),"authorId",id(row,"usr_id"),"author",text(row,"author"),"text",text(row,"ap_text"),"startsAt",dateTime(row,"ap_execdate"),"endsAt",dateTime(row,"ap_enddate"),
|
||||
"status",id(row,"ap_category_1"),"visibility",id(row,"ap_category_2"),"kind",id(row,"ap_category_3"),"monitoring",id(row,"ap_category_4"),"customerId",positiveId(row,"cs_id"),"customerLabel",text(row,"customer_label"),
|
||||
"participants",people.stream().map(id->fields("id",id,"label",names.getOrDefault(id,"Benutzer "+id),"confirmed",confirmed.contains(id))).toList(),"revision",revision(version(row)),"canEdit",canEdit(user,row),"canDelete",id(row,"usr_id")==user.userId()||user.hasRight(3),"canConfirm",people.contains(user.userId())&&!confirmed.contains(user.userId()),"canFinish",canEdit(user,row)&&id(row,"ap_category_4")==1,
|
||||
"hasLegacyRelation",id(row,"csc_id")>0||id(row,"pt_id")>0);
|
||||
}
|
||||
public Map<String,Object> statistics(LegacyUser user,Query query) {
|
||||
access(user);return db.readTransaction(()->{
|
||||
var sql=selection(user,query,true);
|
||||
var rows=db.queryForList("SELECT a.ap_category_1,a.ap_category_2,a.ap_category_3,a.ap_category_4,COUNT(*) AS total FROM "+TABLE+" a WHERE "+sql.where()+" GROUP BY a.ap_category_1,a.ap_category_2,a.ap_category_3,a.ap_category_4 LIMIT 5001",sql.args());
|
||||
if(rows.size()>5000)throw bad("Zu viele Statistikgruppen. Bitte Filter eingrenzen.");
|
||||
var metadata=categories();var categories=new ArrayList<Map<String,Object>>();
|
||||
for(int n=1;n<=4;n++) {
|
||||
var counts=new TreeMap<Long,Long>();for(var row:rows)counts.merge(id(row,"ap_category_"+n),id(row,"total"),Long::sum);
|
||||
var labels=new HashMap<Long,String>();for(var option:metadata.get("category"+n)){long id=id(option,"id");labels.put(id,text(option,"label"));counts.putIfAbsent(id,0L);}
|
||||
categories.add(fields("category",n,"items",counts.entrySet().stream().map(e->fields("id",e.getKey(),"label",labels.getOrDefault(e.getKey(),"Unbekannt ("+e.getKey()+")"),"total",e.getValue())).toList()));
|
||||
}
|
||||
return fields("total",rows.stream().mapToLong(r->id(r,"total")).sum(),"categories",categories,"from",query.from(),"to",query.to());
|
||||
});
|
||||
}
|
||||
Map<String,Object> appointment(LegacyUser user,long appointmentId) {
|
||||
access(user);var hqs=allowedHeadquarters(user);var args=new ArrayList<Object>(List.of(appointmentId));args.addAll(hqs);
|
||||
String where="a.ap_id=? AND a.hq_id IN ("+marks(hqs.size())+") AND "+visible(user,args);
|
||||
return one(db.queryForList("SELECT "+selectColumns()+" FROM "+TABLE+" a WHERE "+where,args.toArray()));
|
||||
}
|
||||
static void editable(LegacyUser user,Map<String,Object> old) { if(!canEdit(user,old))throw new AccessDeniedException("Nur Ersteller, Teilnehmer oder Mitarbeiterverwaltung dürfen diesen Termin ändern."); }
|
||||
static void interval(LocalDateTime starts,LocalDateTime ends) {
|
||||
if(starts==null||ends==null||starts.getYear()<1000||ends.getYear()>9998||ends.isBefore(starts)||starts.plusYears(10).isBefore(ends))throw bad("Bitte einen gültigen Beginn und ein Ende ab Beginn angeben (höchstens zehn Jahre).");
|
||||
if(starts.getSecond()!=0||starts.getNano()!=0||ends.getSecond()!=0||ends.getNano()!=0)throw bad("Termine bitte minutengenau eingeben.");
|
||||
}
|
||||
private Map<String,Object> values(LegacyUser user,Save request,Map<String,Object> old) {
|
||||
if(request==null)throw bad("Termin fehlt.");interval(request.startsAt(),request.endsAt());
|
||||
String text=required(request.text(),65535,"Termintext");if(text.getBytes(StandardCharsets.UTF_8).length>65535)throw bad("Termintext ist zu lang (65535 UTF-8-Bytes).");
|
||||
int[] values={request.status(),request.visibility(),request.kind(),request.monitoring()};var metadata=categories();
|
||||
for(int i=0;i<values.length;i++){int code=values[i];if(code<0||code>127||metadata.get("category"+(i+1)).stream().noneMatch(v->id(v,"id")==code))throw bad("Unbekannte Terminkategorie.");}
|
||||
if(!Set.of(1,2).contains(request.status())||!Set.of(1,2,3).contains(request.visibility())||!Set.of(0,1,2).contains(request.monitoring()))throw bad("Ungültiger Status, Sichtbarkeit oder Kontrolle.");
|
||||
if(request.customerId()!=null) {
|
||||
if(request.customerId()<=0)throw bad("Ungültige Kundenzuordnung.");
|
||||
if(old==null||request.customerId()!=id(old,"cs_id"))customer(user,request.customerId());
|
||||
}
|
||||
if(old!=null&&(id(old,"csc_id")>0||id(old,"pt_id")>0)&&!Objects.equals(request.customerId(),positiveId(old,"cs_id")))throw bad("Die vorhandene Kostenstellen-/Kontaktverknüpfung verhindert eine andere Kundenzuordnung.");
|
||||
if(request.participants()==null||request.participants().size()>500||request.participants().stream().anyMatch(id->id==null||id<=0))throw bad("Bitte höchstens 500 gültige Teilnehmer auswählen.");
|
||||
var people=new TreeSet<>(request.participants());var allowed=new HashSet<Long>();for(var p:employees(allowedHeadquarters(user)))allowed.add(id(p,"id"));
|
||||
if(old!=null)allowed.addAll(GroupAdministration.memberships(text(old,"ap_participants")));
|
||||
if(!allowed.containsAll(people))throw new AccessDeniedException("Ein Teilnehmer ist nicht für die freigegebenen Niederlassungen verfügbar.");
|
||||
var confirmed=old==null?new TreeSet<Long>():new TreeSet<>(GroupAdministration.memberships(text(old,"ap_confirmed")));
|
||||
if(old!=null&&(!request.startsAt().equals(dateTime(old,"ap_execdate"))||!request.endsAt().equals(dateTime(old,"ap_enddate"))))confirmed.clear();
|
||||
confirmed.retainAll(people);
|
||||
return fields("ap_text",text,"ap_execdate",request.startsAt(),"ap_enddate",request.endsAt(),"ap_category_1",request.status(),"ap_category_2",request.visibility(),"ap_category_3",request.kind(),"ap_category_4",request.monitoring(),"cs_id",request.customerId(),"ap_participants",ids(people),"ap_confirmed",ids(confirmed),"ap_modifytime",LocalDateTime.now(ZoneId.of("Europe/Berlin")).withNano(0));
|
||||
}
|
||||
private static Long positiveId(Map<String,Object> row,String key) { long value=id(row,key);return value>0?value:null; }
|
||||
static String ids(Collection<Long> ids) { return ids.isEmpty()?"":","+String.join(",",ids.stream().map(String::valueOf).toList())+","; }
|
||||
public Map<String,Object> create(LegacyUser user,Save request) {
|
||||
access(user);var fields=values(user,request,null);
|
||||
if(request.monitoring()==2)throw bad("Neue Termine bitte zunächst ohne Kontrolle oder mit aktiver Überwachung anlegen.");
|
||||
fields.putAll(fields("usr_id",user.userId(),"hq_id",user.headquartersId(),"ap_type",0,"csc_id",null,"pt_id",null));
|
||||
long id=db.writeTransaction(()->db.insert(TABLE,fields));return fields("id",id);
|
||||
}
|
||||
public Map<String,Object> save(LegacyUser user,long appointmentId,Save request) {
|
||||
access(user);return db.writeTransaction(()->{
|
||||
var old=appointment(user,appointmentId);editable(user,old);if(request==null)throw bad("Termin fehlt.");unchanged(version(old),request.revision());
|
||||
var values=values(user,request,old);
|
||||
if(id(old,"ap_category_4")!=2&&request.monitoring()==2) {
|
||||
// Check the resulting customer/status too; changing the assignment must not bypass the report rule.
|
||||
var completed=new LinkedHashMap<>(old);completed.putAll(values);requireReport(user,completed);
|
||||
}
|
||||
update(old,values);return fields("id",appointmentId);
|
||||
});
|
||||
}
|
||||
public void remove(LegacyUser user,long appointmentId,String revision) {
|
||||
access(user);db.writeTransaction(()->{
|
||||
var old=appointment(user,appointmentId);if(id(old,"usr_id")!=user.userId())user.require(3);unchanged(version(old),revision);
|
||||
changed(db.update("DELETE FROM "+TABLE+" WHERE "+cas(),casArgs(old).toArray()));return null;
|
||||
});
|
||||
}
|
||||
public Map<String,Object> resubmit(LegacyUser user,long appointmentId,Resubmission request) {
|
||||
access(user);if(request==null)throw bad("Wiedervorlage fehlt.");interval(request.startsAt(),request.endsAt());
|
||||
var old=appointment(user,appointmentId);unchanged(version(old),request.revision());
|
||||
// A resubmission creates a new appointment. Its source and source confirmations stay untouched.
|
||||
var values=new LinkedHashMap<>(version(old));values.remove("ap_id");values.put("usr_id",user.userId());values.put("hq_id",user.headquartersId());
|
||||
values.put("ap_execdate",request.startsAt());values.put("ap_enddate",request.endsAt());values.put("ap_confirmed","");values.put("ap_modifytime",LocalDateTime.now(ZoneId.of("Europe/Berlin")).withNano(0));
|
||||
if(id(old,"ap_category_4")==2)values.put("ap_category_4",1);
|
||||
long id=db.writeTransaction(()->db.insert(TABLE,values));return fields("id",id);
|
||||
}
|
||||
public void confirm(LegacyUser user,long appointmentId,String revision) {
|
||||
access(user);db.writeTransaction(()->{
|
||||
var old=appointment(user,appointmentId);
|
||||
if(!GroupAdministration.memberships(text(old,"ap_participants")).contains(user.userId()))throw new AccessDeniedException("Nur eigene Teilnahme kann bestätigt werden.");
|
||||
unchanged(version(old),revision);var confirmed=new TreeSet<>(GroupAdministration.memberships(text(old,"ap_confirmed")));confirmed.add(user.userId());
|
||||
update(old,fields("ap_confirmed",ids(confirmed)));return null;
|
||||
});
|
||||
}
|
||||
public void finish(LegacyUser user,long appointmentId,String revision) {
|
||||
access(user);db.writeTransaction(()->{
|
||||
var old=appointment(user,appointmentId);editable(user,old);unchanged(version(old),revision);
|
||||
if(id(old,"ap_category_4")!=1)throw bad("Nur ein überwachter Termin kann abgeschlossen werden.");requireReport(user,old);
|
||||
update(old,fields("ap_category_4",2,"ap_modifytime",LocalDateTime.now(ZoneId.of("Europe/Berlin")).withNano(0)));return null;
|
||||
});
|
||||
}
|
||||
private void requireReport(LegacyUser user,Map<String,Object> appointment) {
|
||||
long customerId=id(appointment,"cs_id");
|
||||
if(customerId>0&&id(appointment,"ap_category_1")==1&&catalog.enabled(0,"MASK_AP_WARNING_DISABLE_QUESTION_NO_REPORT_OF_SAME_DAY")) {
|
||||
var today=LocalDate.now(ZoneId.of("Europe/Berlin"));var args=new ArrayList<Object>(List.of(customerId,id(appointment,"hq_id"),today.atStartOfDay(),today.plusDays(1).atStartOfDay()));
|
||||
String sql="SELECT rp_id FROM `phoenix_group`.report_process WHERE rp_objtype='cs' AND rp_objid=? AND hq_id=? AND rp_createtime>=? AND rp_createtime<?";
|
||||
if(!user.hasRight(10)){sql+=" AND (rp_confidential=0 OR usr_id=?)";args.add(user.userId());}
|
||||
if(db.queryForList(sql+" LIMIT 1",args.toArray()).isEmpty())throw bad("Bitte zuerst einen Kundenbericht für den heutigen Tag anlegen.");
|
||||
}
|
||||
}
|
||||
private void update(Map<String,Object> old,Map<String,Object> values) {
|
||||
var args=new ArrayList<>(values.values());args.addAll(casArgs(old));
|
||||
changed(db.update("UPDATE "+TABLE+" SET "+String.join(",",values.keySet().stream().map(c->c+"=?").toList())+" WHERE "+cas(),args.toArray()));
|
||||
}
|
||||
// MyISAM does not support transactional row locks. Include all source values in each mutation.
|
||||
private static String cas() { return String.join(" AND ",COLUMNS.stream().map(c->Set.of("ap_text","ap_participants","ap_confirmed").contains(c)?"BINARY "+c+" <=> BINARY ?":c+" <=> ?").toList()); }
|
||||
private static List<Object> casArgs(Map<String,Object> old) { var args=new ArrayList<Object>();for(String c:COLUMNS)args.add(value(old,c));return args; }
|
||||
private static void changed(int rows) { if(rows!=1)throw new ResponseStatusException(HttpStatus.CONFLICT,"Der Termin wurde zwischenzeitlich geändert oder gelöscht. Bitte neu laden."); }
|
||||
}
|
||||
Reference in new issue
Block a user