package com.example.schedulerapp; import java.net.*; import java.io.*; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class Model { private PrintWriter writer; private Socket socket; protected Map<Integer, Shift> shifts; protected Map<Integer, Employee> employees; protected Map<Integer, TimeOff> timeOff; protected Map<String, Float> positions; protected ArrayList<ModelSubscriber> subscribers; static LocalDate date; private boolean isManager; private int thisEmployee; private int selectedEmployee; public Model(String hostname, int port) { subscribers = new ArrayList<>(); if(date == null) { date = LocalDate.now(); } isManager = false; // changes if manager logs in this.shifts = new HashMap<>(); this.employees = new HashMap<>(); this.timeOff = new HashMap<>(); this.positions = new HashMap<>(); try { InetAddress ip = InetAddress.getByName(hostname); this.socket = new Socket(ip, port); System.out.println("Connected to the scheduling server"); new ReceiveThread(this.socket, this).start(); OutputStream output = this.socket.getOutputStream(); this.writer = new PrintWriter(output, true); } catch (UnknownHostException error) { System.out.println("Server not found: " + error.getMessage()); } catch (IOException error) { System.out.println("I/O Error: " + error.getMessage()); } } public void setIsManager(boolean aBool){ isManager = aBool; } public boolean getIsManager(){ return isManager; } public void setThisEmployee(int id){ thisEmployee = selectedEmployee = id; } public int getThisEmployee(){ return thisEmployee; } //for manager only, allows viewing of weekly schedule by employee public void setSelectedEmployee(int id){ selectedEmployee = id; notifySubscribers(); } public int getSelectedEmployee(){ return selectedEmployee; } public void dateNext(){ date = date.plusDays(1); notifySubscribers(); } public void weekNext(){ date = date.plusDays(7); notifySubscribers(); } public void datePrev(){ date = date.minusDays(1); notifySubscribers(); } public void weekPrev(){ date = date.minusDays(7); notifySubscribers(); } public LocalDate getDate(){ return date; } //todo: add calender to fill jump field public void dateJump(LocalDate date){ this.date = date; notifySubscribers(); } /* Name: addEmployee Parameters: String firstName: The new employees first name. String lastName: The new employees last name. Description: Adds an employee to the database. Return: String indicating the result */ String addEmployee(String firstName, String lastName) { if (firstName.contains("/") || lastName.contains("/") || firstName.contains(",") || lastName.contains(",")){ return "Illegal character(s)"; } else { writer.println("addEmployee/" + firstName + "/" + lastName + "/" + false + "/" + "empty" + "/" + "empty" + "/" + 0.0f); notifySubscribers(); return "Employee added"; } } @Deprecated void addEmployee(String firstName, String lastName, boolean isManager, String email, String phoneNumber, float wage) { writer.println("addEmployee/" + firstName + "/" + lastName + "/" + isManager + "/" + email + "/" + phoneNumber + "/" + wage); } String editEmployee(int employeeID, String firstName, String lastName, boolean isManager, String email, String phoneNumber, float wage) { if (employeeID <= 0){ return "Invalid ID"; } if (firstName.contains("/") || lastName.contains("/") || firstName.contains(",") || lastName.contains(",")){ return "Name contains illegal character(s)"; } if(email.contains("/") || email.contains(",")){ return "Email contains illegal character(s)"; } if(phoneNumber.contains("/") || phoneNumber.contains(",") || phoneNumber.contains("-")){ return "phoneNumber contains illegal character(s)"; } if (wage < 0){ return "Invalid wage"; } if (this.employees.containsKey(employeeID)) { writer.println("editEmployee/" + employeeID + "/" + firstName + "/" + lastName + "/" + isManager + "/" + email + "/" + phoneNumber + "/" + wage); return "Employee edited"; } return "Employee does not exist"; } /* Name: removeEmployee Parameters: String ID: The employees unique ID. Description: Removes an employee with the given ID if it exists. Return: String indicating the result */ String removeEmployee(String employeeID) { if (Integer.parseInt(employeeID) <= 0){ return "Invalid ID"; } if (this.employees.containsKey(Integer.parseInt(employeeID))) { writer.println("removeEmployee/" + employeeID); return "Employee removed"; } return "Employee does not exist"; } /* Name: getEmployee Parameters: int ID: The employees unique ID. Description: Gets an employee with the given ID if it exists. Return: The employee object if it exists, null if not */ Employee getEmployee(int employeeID) { return employees.getOrDefault(employeeID, null); } /* Sorry if this breaks anything. */ void printAllEmployees(){ if (employeeSize() == 0){ System.out.println("No employees"); } ArrayList<String> employeeList = new ArrayList<>(); for (Employee employee : this.employees.values()) { employeeList.add(employee.getFullName()); } System.out.println(employeeList); } /* Name: employeeSize Description: Gets the number of employees in the database. Return: The total number of employees. */ int employeeSize(){ return this.employees.size(); } /* Name: getEmployeeByID Parameters: int ID: The employees unique ID. Description: Gets an employee's name with the given ID if it exists. Return: A string containing the employees full name */ String getEmployeeByID(int id) { if (id <= 0){ return "Invalid ID"; } if (this.employees.containsKey(id)) { Employee employee = this.employees.get(id); return employee.getFullName(); } return "No Employee with that ID"; } public ArrayList<Employee> getAllEmployees(){ return new ArrayList<>(employees.values()); } /* Name: addShift Parameters: int ID: The employees unique ID. String date: The date in yyyy-mm-dd format. int start: The start time in 24 hour time. int end: The end time in 24 hour time. Description: Adds a new shift on the given date. Return: String indicating the result */ @Deprecated String addShift(int id, String date, int start, int end) { if (id <= 0) { return "Invalid ID"; } if (start < 0 || end < 0 || end > 2359 || start >= end) { return "Invalid start or end time"; } //todo: fix this, broke when moved from int to localtime /* DateTimeFormatter dbTimeFormat = DateTimeFormatter.ofPattern("kkmm"); LocalTime startTime = LocalTime.parse(String.valueOf(start), dbTimeFormat); LocalTime endTime = LocalTime.parse(String.valueOf(end), dbTimeFormat); if (this.employees.containsKey(id)) { LocalTime[] dayOfTheWeekAvailability = this.employees.get(id).getAvailabilityForDay(LocalDate.parse(date) .getDayOfWeek().getValue() % 7); if (dayOfTheWeekAvailability[0].isAfter(startTime) || dayOfTheWeekAvailability[1].isBefore(startTime) || dayOfTheWeekAvailability[1].isBefore(endTime)) { return "Shift time conflicts with employees availability"; } } */ if (this.employees.containsKey(id)) { for (Shift shift : getShiftsByDay(date)) { if (shift.getEmployeeID() == id) { /* Commented out split shift just in case int otherShiftStart = Integer.parseInt(shift.getStart().toLocalTime().toString().replace(":", "")); int otherShiftEnd = Integer.parseInt(shift.getEnd().toLocalTime().toString().replace(":", "")); if (start <= otherShiftEnd && !(end <= otherShiftStart)){ return "Shift time conflict with existing shift"; } */ return "Shift time conflict for employee"; } } } else { return "No Employee with that ID"; } writer.println("addShift/" + id + "/" + date + "/" + start + "/" + end + "/" + " "); //todo local? notifySubscribers(); return "New shift added"; } boolean hasEmployee(int id){ return employees.containsKey(id); } // String addShift(int id, String date, int start, int end, String position){ if (id <= 0) { return "Invalid ID"; } if (start < 0 || end < 0 || end > 2359 || start >= end) { return "Invalid start or end time"; } if (end < start){ return "End time must be after start time"; } //todo: fix this, broke when moved from int to localtime /* DateTimeFormatter dbTimeFormat = DateTimeFormatter.ofPattern("kkmm"); LocalTime startTime = LocalTime.parse(String.valueOf(start), dbTimeFormat); LocalTime endTime = LocalTime.parse(String.valueOf(end), dbTimeFormat); if (this.employees.containsKey(id)) { LocalTime[] dayOfTheWeekAvailability = this.employees.get(id).getAvailabilityForDay(LocalDate.parse(date) .getDayOfWeek().getValue() % 7); if (dayOfTheWeekAvailability[0].isAfter(startTime) || dayOfTheWeekAvailability[1].isBefore(startTime) || dayOfTheWeekAvailability[1].isBefore(endTime)) { return "Shift time conflicts with employees availability"; } } */ if (this.employees.containsKey(id)) { for (Shift shift : getShiftsByDay(date)) { if (shift.getEmployeeID() == id) { /* Commented out split shift just in case int otherShiftStart = Integer.parseInt(shift.getStart().toLocalTime().toString().replace(":", "")); int otherShiftEnd = Integer.parseInt(shift.getEnd().toLocalTime().toString().replace(":", "")); if (start <= otherShiftEnd && !(end <= otherShiftStart)){ return "Shift time conflict with existing shift"; } */ return "Shift time conflict for employee"; } } } else { return "No Employee with that ID"; } for (int timeOffID : getEmployee(id).getTimeOffID()) { if (this.timeOff.get(timeOffID).isApproval() && this.timeOff.get(timeOffID).conflictsWithTimeOff(date)) { return "Shift time conflicts with approved time off"; } } writer.println("addShift/"+id+"/"+date+"/"+start+"/"+end+"/"+position); notifySubscribers(); return "New shift added"; } String addShiftIgnore(int id, String date, int start, int end, String position) { if (start < 0 || end < 0 || end > 2359 || start >= end) { return "Invalid start or end time"; } if (this.employees.containsKey(id)) { for (Shift shift : getShiftsByDay(date)) { if (shift.getEmployeeID() == id) { /* Commented out split shift just in case int otherShiftStart = Integer.parseInt(shift.getStart().toLocalTime().toString().replace(":", "")); int otherShiftEnd = Integer.parseInt(shift.getEnd().toLocalTime().toString().replace(":", "")); if (start <= otherShiftEnd && !(end <= otherShiftStart)){ return "Shift time conflict with existing shift"; } */ return "Shift time conflict for employee"; } } } else { return "No Employee with that ID"; } writer.println("addShift/"+id+"/"+date+"/"+start+"/"+end+"/"+position); notifySubscribers(); return "New shift added"; } /* Name: removeShift Parameters: String shiftID: The unique ID for the shift. Description: Removes the shift with the given ID if it exists. Return: String indicating the result */ String removeShift(int shiftID){ if (shiftID <= 0){ return "Invalid ID"; } if (this.shifts.containsKey(shiftID)) { writer.println("removeShift/" + shiftID); notifySubscribers(); return "Shift removed"; } return "Shift does not exist"; } String editShift(int shiftID, String day, int start, int end, Boolean availability, int employeeID) { if (shiftID <= 0){ return "Invalid shift ID"; } if (employeeID <= 0){ return "Invalid employee ID"; } if (start < 0 || end < 0 || end > 2400 || start >= end){ return "Invalid start or end time"; } if (!this.employees.containsKey(employeeID)) { return "Employee doesn't exist"; } for (int timeOffID : getEmployee(employeeID).getTimeOffID()) { if (this.timeOff.get(timeOffID).isApproval() && this.timeOff.get(timeOffID).conflictsWithTimeOff(day)) { return "Shift time conflicts with approved time off"; } } //todo: fix this, broke when moved from int to localtime /* DateTimeFormatter dbTimeFormat = DateTimeFormatter.ofPattern("kkmm"); LocalTime startTime = LocalTime.parse(String.valueOf(start), dbTimeFormat); LocalTime endTime = LocalTime.parse(String.valueOf(end), dbTimeFormat); if (this.employees.containsKey(id)) { LocalTime[] dayOfTheWeekAvailability = this.employees.get(id).getAvailabilityForDay(LocalDate.parse(date) .getDayOfWeek().getValue() % 7); if (dayOfTheWeekAvailability[0].isAfter(startTime) || dayOfTheWeekAvailability[1].isBefore(startTime) || dayOfTheWeekAvailability[1].isBefore(endTime)) { return "Shift time conflicts with employees availability"; } } */ if (this.shifts.containsKey(shiftID)) { writer.println("editShift/" + shiftID + "/" + day + "/" + start + "/" + end + "/" + availability + "/" + employeeID); notifySubscribers(); return "Shift successfully edited"; } return "Shift does not exist"; } String editShiftIgnore(int shiftID, String day, int start, int end, Boolean availability, int employeeID) { if (shiftID <= 0){ return "Invalid shift ID"; } if (employeeID <= 0){ return "Invalid employee ID"; } if (start < 0 || end < 0 || end > 2400 || start >= end){ return "Invalid start or end time"; } if (!this.employees.containsKey(employeeID)) { return "Employee doesn't exist"; } if (this.shifts.containsKey(shiftID)) { writer.println("editShift/" + shiftID + "/" + day + "/" + start + "/" + end + "/" + availability + "/" + employeeID); notifySubscribers(); return "Shift successfully edited"; } return "Shift does not exist"; } /* Name: getShift Parameters: int shiftID: The unique shift ID Description: Gets the shift with the given ID if it exists. Return: The shift object if it exists, null if not */ Shift getShift(int shiftID) { return shifts.getOrDefault(shiftID, null); } void printAllShifts() { System.out.println(this.shifts); } /* Parameters: - String date in format 2022-03-10. Return: ArrayList<String> where each string is a shift of format id.yyyy-mm-dd.start.end */ ArrayList<Shift> getShiftsByDay(String date){ ArrayList<Shift> shifts = new ArrayList<>(); for (Shift shift : this.shifts.values()) { if (shift.getDate().equals(date)) { shifts.add(shift); } } return shifts; } /* Parameters: startOfWeek - The first day of the week in "yyyy-mm-dd" format endOfWeek - The last day of the week in "yyyy-mm-dd" format Return: ArrayList<String> */ ArrayList<Shift> getShiftsByWeek(LocalDateTime startOfWeek, LocalDateTime endOfWeek){ ArrayList<Shift> shifts = new ArrayList<>(); for (Shift shift : this.shifts.values()) { LocalDateTime start = shift.getStart(); LocalDateTime end = shift.getEnd(); if ((start.isAfter(startOfWeek) || start.isEqual(startOfWeek)) && (end.isBefore(endOfWeek)) || end.isEqual(endOfWeek)) { shifts.add(shift); } } return shifts; } //for view public ArrayList<Shift> getWeeklySchedule(){ LocalDateTime start; LocalDateTime end; // get start and end dates to pass along int today = date.getDayOfWeek().getValue(); // Mon is 1, Sun is 7 if (today == 7) { //this is the start date start = date.atTime(0, 0); end = date.plusDays(6).atTime(0,0); } else { start = date.minusDays(today).atTime(0, 0); end = date.plusDays(6 - today).atTime(0, 0); } return getEmployeeShiftsByWeek(selectedEmployee, start, end); } //for view public ArrayList<Shift> getDailySchedule(){ return getShiftsByDay(date.toString()); } ArrayList<Shift> getEmployeeShifts(String id) { int intID = -1; try { intID = Integer.parseInt(id); } catch (NumberFormatException exception) { exception.printStackTrace(); } ArrayList<Shift> shifts = new ArrayList<>(); for (Shift shift: this.shifts.values()) { if (shift.getEmployeeID() == intID) { shifts.add(shift); } } return shifts; } ArrayList<Shift> getEmployeeShiftsByWeek(int employeeID, LocalDateTime startOfWeek, LocalDateTime endOfWeek){ ArrayList<Shift> shifts = new ArrayList<>(); for (Shift shift : this.shifts.values()) { LocalDateTime start = shift.getStart(); LocalDateTime end = shift.getEnd(); if ((shift.getEmployeeID() == employeeID) && (start.isAfter(startOfWeek) || start.isEqual(startOfWeek)) && (end.isBefore(endOfWeek)) || end.isEqual(endOfWeek)) { shifts.add(shift); } } return shifts; } String editShiftAvailability(int shiftID, boolean available) { if (shiftID <= 0){ return "Invalid shift ID"; } if (this.shifts.containsKey(shiftID)) { writer.println("editShiftAvailability/" + shiftID + "/" + available); return "Shift edited"; } return "Shift does not exist"; } // adds a position if its one of the possible positions in the position table. String addEmployeePosition(int employeeID, String position) { if (employeeID <= 0){ return "Invalid employee ID"; } if (!this.positions.containsKey(position)){ return "Position does not exist"; } if (this.employees.containsKey(employeeID)) { writer.println("addEmployeePosition/" + employeeID + "/" + position); return "Position added to employee"; } return "Employee does not exist"; } String removeEmployeePosition(int employeeID, String position) { if (employeeID <= 0){ return "Invalid employee ID"; } if (!this.positions.containsKey(position)){ return "Position does not exist"; } if (this.employees.containsKey(employeeID)) { writer.println("removeEmployeePosition/" + employeeID + "/" + position); return "Position removed from employee"; } return "Employee does not exist"; } String editAvailability(int employeeID, int dayOfTheWeek, int start, int end) { if (employeeID <= 0){ return "Invalid employee ID"; } if (dayOfTheWeek < 0 || dayOfTheWeek > 8){ return "Invalid day"; } if (end <= start){ return "End must be after start"; } if (start < 0 || start > 2400 || end > 2400){ return "Invalid start or end time"; } if (this.employees.containsKey(employeeID)) { writer.println("editAvailability/" + employeeID + "/" + dayOfTheWeek + "/" + start + "/" + end); editLocalAvailability(employeeID, dayOfTheWeek, start, end); notifySubscribers(); return "Availability changed successfully"; } return "Employee does not exist"; } private void editLocalAvailability(int employeeID, int dayOfTheWeek, int start, int end){ employees.get(employeeID).updateAvailability(dayOfTheWeek, intToLocalTime(start), intToLocalTime(end)); } public LocalTime intToLocalTime(int time){ String resultString; String sub = "0000"; String timeString = String.valueOf(time); if (timeString.length() < 4){ resultString = sub.substring(0, 4-timeString.length()) + timeString; } else { resultString = timeString; } DateTimeFormatter dbTimeFormat = DateTimeFormatter.ofPattern("kkmm"); LocalTime result = LocalTime.parse(resultString, dbTimeFormat); return result; } //"YYYY-MM-DD" String addTimeOff(int employeeID, String startDate, String endDate, String reason) { if (employeeID <= 0){ return "Invalid employee ID"; } if (reason.contains("/") || reason.contains(",")){ return "Illegal character(s)"; } if (this.employees.containsKey(employeeID)) { writer.println("addTimeOff/" + employeeID + "/" + startDate + "/" + endDate + "/" + reason); return "Time off added"; } return "Employee does not exist"; } String setTimeOffApproval(int timeOffID, boolean approval) { if (timeOffID <= 0){ return "Invalid employee ID"; } if(this.timeOff.containsKey(timeOffID)) { writer.println("setTimeOffApproval/" + timeOffID + "/" + approval); return "Time off approved"; } return "Time off request doesn't exist"; } String removeTimeOff(int timeOffID) { if (timeOffID <= 0){ return "Invalid employee ID"; } if(this.timeOff.containsKey(timeOffID)) { writer.println("removeTimeOff/" + timeOffID); return "Time off removed"; } return "Time off request does not exist"; } String addPosition(String position, float wage) { if (position.contains("/") || position.contains(",")){ return "Illegal character(s)"; } if (wage <= 0){ return "Invalid wage"; } writer.println("addPosition/" + position + "/" + wage); System.out.println("add positions call - positions size: " + this.positions.keySet().size()); return "Position added"; } String editPositionWage(String position, float wage) { if (position.contains("/") || position.contains(",")){ return "Illegal character(s)"; } if (wage <= 0){ return "Invalid wage"; } if(this.positions.containsKey(position)) { writer.println("editPosition/" + position + "/" + wage); return "Position edited"; } return "Position does not exist"; } String removePosition(String position) { if (position == null){ return "No position selected."; } if (position.contains("/") || position.contains(",")){ return "Illegal character(s)"; } if(this.positions.containsKey(position)) { writer.println("removePosition/"+ position + "/" + positions.get(position)); return "Position removed"; } return "Position does not exist"; } //How this works is if you start within 5 minutes of your shift than you can "signed-in" and //assumes you work the whole shift, if you leave early of come in late the shift manager //must change it in the schedule. void checkIn(int shiftID) { LocalDateTime nMinutesAfter = this.shifts.get(shiftID).getStart().plusMinutes(5); LocalDateTime nMinutesBefore = this.shifts.get(shiftID).getStart().minusMinutes(5); LocalDateTime now = LocalDateTime.now(); if (nMinutesBefore.isBefore(now) && nMinutesAfter.plusMinutes(5).isAfter(now)) { writer.println("checkIn/" + shiftID); } } //This allows the manager to mark any shift as checked-in. void managerCheckIn(int shiftID) { writer.println("checkIn/" + shiftID); } //The dates need to be in form "yyyy-mm-dd" //Also the start and end are inclusive. float getHoursWorked(int employeeID, String start, String end) { float hoursWorked = 0.0f; try { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); Date startDate = formatter.parse(start); Date endDate = formatter.parse(end); for (Shift shift : this.shifts.values()) { if (shift.getEmployeeID() == employeeID) { Date shiftDate = formatter.parse(shift.getDate()); if (!startDate.after(shiftDate) && !endDate.before(shiftDate)){ if (shift.isCheckedIn()) { hoursWorked = (hoursWorked + shift.getEnd().getHour() - shift.getStart().getHour()) + ((shift.getEnd().getMinute() - shift.getStart().getMinute())/60f); } } } } } catch (Exception exception) { exception.printStackTrace(); return -1f; } return hoursWorked; } void logOut() { writer.println("logout"); try { this.socket.close(); } catch (IOException ex){ System.out.println("Error: Failed to close socket"); } } public static void main(String[] args) { String hostname = "localhost"; int port = 8989; Model client = new Model(hostname, port); System.out.print("Retrieving Data"); while(client.employees.isEmpty()){ System.out.print("."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { System.out.println("Error: ScheduleClient main. Interrupted while retrieving data "); } } System.out.println(); System.out.println("Data retrieved."); System.out.println("ScheduleClient is ready to use!"); } public ArrayList<String> returnFormattedEmployeeNames(){ ArrayList<String> result = new ArrayList<>(); for (Employee employee: this.employees.values()) { result.add(employee.getFullName()); } //System.out.println(result); return result; } public ArrayList<String> returnPositions(){ System.out.println("positions size: " + this.positions.keySet().size()); return new ArrayList<>(this.positions.keySet()); } public int getIDbyIndex(int index) { Employee[] employeeList = this.employees.values().toArray(new Employee[0]); System.out.println(employeeList[index].getEmployeeID()); return employeeList[index].getEmployeeID(); } //for publish/subscribe public void addSubscriber(ModelSubscriber sub){ subscribers.add(sub); } public void notifySubscribers(){ for (ModelSubscriber sub : subscribers){ sub.modelChanged(); } } }