├── .travis.yml ├── assets └── screencast.gif ├── src └── main │ ├── resources │ ├── public │ │ └── favicon.ico │ ├── VAADIN │ │ └── themes │ │ │ └── valo │ │ │ ├── favicon.ico │ │ │ └── img │ │ │ └── toggl-logo.png │ ├── application.properties │ ├── templates │ │ └── login.html │ └── io │ │ └── rocketbase │ │ └── toggl │ │ └── ui │ │ └── design.css │ └── java │ └── io │ └── rocketbase │ └── toggl │ ├── backend │ ├── model │ │ ├── worker │ │ │ ├── ContactType.java │ │ │ ├── Contact.java │ │ │ └── ContractTerms.java │ │ ├── leave │ │ │ ├── LeaveStatus.java │ │ │ └── LeaveType.java │ │ ├── global │ │ │ └── Note.java │ │ ├── DateTimeEntryGroup.java │ │ ├── Worker.java │ │ ├── report │ │ │ ├── WeekTimeline.java │ │ │ └── UserTimeline.java │ │ ├── LeaveEntry.java │ │ ├── DailyWorkingLog.java │ │ └── ApplicationSetting.java │ ├── repository │ │ ├── WorkerRepository.java │ │ ├── DailyWorkingLogRepository.java │ │ ├── ApplicationSettingRepository.java │ │ ├── MongoUserDetailsRepository.java │ │ ├── LeaveEntryRepository.java │ │ └── DateTimeEntryGroupRepository.java │ ├── security │ │ ├── UserRole.java │ │ ├── MongoUserDetailsService.java │ │ ├── MongoUserDetails.java │ │ └── MongoUserService.java │ ├── controller │ │ ├── RootController.java │ │ └── LoginController.java │ ├── util │ │ ├── LocalDateConverter.java │ │ ├── YearWeekUtil.java │ │ ├── YearMonthUtil.java │ │ └── ColorPalette.java │ ├── service │ │ ├── WorkerService.java │ │ ├── HolidayManagerService.java │ │ ├── FetchAndStoreService.java │ │ └── TimeEntryService.java │ ├── scheduler │ │ └── PullTimeEntriesScheduler.java │ └── config │ │ ├── SecurityConfiguration.java │ │ └── TogglService.java │ ├── Application.java │ └── ui │ ├── view │ ├── error │ │ └── ErrorView.java │ ├── AbstractView.java │ ├── setting │ │ ├── window │ │ │ └── LinkWorkerWindow.java │ │ ├── SettingView.java │ │ ├── tab │ │ │ ├── SchedulingTab.java │ │ │ ├── SettingTab.java │ │ │ ├── LoginUserTab.java │ │ │ └── PullDataTab.java │ │ └── form │ │ │ ├── UserDetailForm.java │ │ │ └── MongoUserForm.java │ ├── home │ │ ├── HomeView.java │ │ └── tab │ │ │ ├── ChartTab.java │ │ │ ├── MonthStatisticsTab.java │ │ │ └── WeekStatisticsTab.java │ └── worker │ │ ├── form │ │ ├── WorkerForm.java │ │ └── ContractTermsForm.java │ │ └── WorkerView.java │ ├── component │ ├── tab │ │ ├── AbstractTab.java │ │ └── ExtendedTabSheet.java │ ├── CustomViewAccessControl.java │ ├── MainScreen.java │ ├── NoteComponent.java │ └── Menu.java │ └── MainUI.java ├── Dockerfile ├── .gitignore ├── README.md └── pom.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | notifications: 4 | email: false -------------------------------------------------------------------------------- /assets/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketbase-io/toggl-reporter/HEAD/assets/screencast.gif -------------------------------------------------------------------------------- /src/main/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketbase-io/toggl-reporter/HEAD/src/main/resources/public/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/VAADIN/themes/valo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketbase-io/toggl-reporter/HEAD/src/main/resources/VAADIN/themes/valo/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/VAADIN/themes/valo/img/toggl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketbase-io/toggl-reporter/HEAD/src/main/resources/VAADIN/themes/valo/img/toggl-logo.png -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/worker/ContactType.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.worker; 2 | 3 | public enum ContactType { 4 | PRIVATE, BUSINESS; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/leave/LeaveStatus.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.leave; 2 | 3 | public enum LeaveStatus { 4 | 5 | REQUESTED, ACCEPTED, REJECTED; 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | VOLUME /tmp 4 | ARG JAR_FILE 5 | ADD target/${JAR_FILE} app.jar 6 | EXPOSE 8080 7 | 8 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/leave/LeaveType.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.leave; 2 | 3 | public enum LeaveType { 4 | 5 | VACATION, SPECIAL_LEAVE, UNPAID_LEAVE, SICKNESS, MEDICAL_CERTIFICATE; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.data.mongodb.uri=mongodb://localhost/toggl-report 2 | 3 | application.title=Toggl Reporter 4 | 5 | vaadin.servlet.productionMode=true 6 | vaadin.servlet.heartbeatInterval=60 7 | vaadin.servlet.closeIdleSessions=true -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/WorkerRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.model.Worker; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface WorkerRepository extends MongoRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/DailyWorkingLogRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.model.DailyWorkingLog; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface DailyWorkingLogRepository extends MongoRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/security/UserRole.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.security; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | 5 | public enum UserRole implements GrantedAuthority { 6 | 7 | ROLE_USER, 8 | ROLE_ADMIN; 9 | 10 | public String getAuthority() { 11 | return name(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/ApplicationSettingRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.model.ApplicationSetting; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface ApplicationSettingRepository extends MongoRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ 25 | 26 | rebel.xml 27 | 28 | dockerize.sh -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/MongoUserDetailsRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface MongoUserDetailsRepository extends MongoRepository { 9 | 10 | Optional findByUsername(String username); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/LeaveEntryRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.model.LeaveEntry; 4 | import io.rocketbase.toggl.backend.model.Worker; 5 | import org.springframework.data.mongodb.repository.MongoRepository; 6 | 7 | import java.util.List; 8 | 9 | public interface LeaveEntryRepository extends MongoRepository { 10 | 11 | List findAllByWorker(Worker worker); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/global/Note.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.global; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.joda.time.DateTime; 8 | 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @Builder 14 | public class Note { 15 | 16 | private String title; 17 | 18 | private String body; 19 | 20 | private DateTime created; 21 | 22 | private String username; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/worker/Contact.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.worker; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @Builder 12 | public class Contact { 13 | 14 | private String emailAddress; 15 | 16 | private String phone; 17 | 18 | private String street; 19 | 20 | private String postcode; 21 | 22 | private String city; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/controller/RootController.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | 8 | @Controller 9 | public class RootController { 10 | 11 | @RequestMapping(value = "/", method = RequestMethod.GET) 12 | public String list(Model model) { 13 | return "redirect:/app"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/repository/DateTimeEntryGroupRepository.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.repository; 2 | 3 | import io.rocketbase.toggl.backend.model.DateTimeEntryGroup; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | /** 10 | * Created by marten on 08.03.17. 11 | */ 12 | 13 | public interface DateTimeEntryGroupRepository extends MongoRepository { 14 | 15 | Long deleteByWorkspaceIdAndDateBetween(long workspaceId, Date from, Date to); 16 | 17 | List findByWorkspaceIdAndDateBetween(long workspaceId, Date from, Date to); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/util/LocalDateConverter.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.util; 2 | 3 | import org.joda.time.format.ISODateTimeFormat; 4 | 5 | import java.time.LocalDate; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | public final class LocalDateConverter { 9 | 10 | public static LocalDate convert(org.joda.time.LocalDate localDate) { 11 | return LocalDate.of(localDate.getYear(), localDate.getMonthOfYear(), localDate.getDayOfMonth()); 12 | } 13 | 14 | public static org.joda.time.LocalDate convert(LocalDate localDate) { 15 | return org.joda.time.LocalDate.parse(localDate.format(DateTimeFormatter.BASIC_ISO_DATE), ISODateTimeFormat.basicDate()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/service/WorkerService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.service; 2 | 3 | import io.rocketbase.toggl.backend.model.Worker; 4 | import io.rocketbase.toggl.backend.repository.WorkerRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.annotation.Resource; 8 | import java.util.List; 9 | 10 | @Service 11 | public class WorkerService { 12 | 13 | @Resource 14 | private WorkerRepository workerRepository; 15 | 16 | public List findAll() { 17 | return workerRepository.findAll(); 18 | } 19 | 20 | public Worker updateWorker(Worker worker) { 21 | return workerRepository.save(worker); 22 | } 23 | 24 | public void deleteWorker(Worker worker) { 25 | workerRepository.delete(worker); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/Application.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 8 | 9 | @SpringBootApplication 10 | @EnableScheduling 11 | public class Application extends WebMvcConfigurerAdapter { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | 17 | @Override 18 | public void addViewControllers(ViewControllerRegistry registry) { 19 | registry.addViewController("/login") 20 | .setViewName("login"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/error/ErrorView.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.error; 2 | 3 | import com.vaadin.navigator.View; 4 | import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; 5 | import com.vaadin.ui.Alignment; 6 | import com.vaadin.ui.CustomComponent; 7 | import com.vaadin.ui.themes.ValoTheme; 8 | import org.vaadin.viritin.MSize; 9 | import org.vaadin.viritin.label.MLabel; 10 | import org.vaadin.viritin.layouts.MVerticalLayout; 11 | 12 | public class ErrorView extends CustomComponent implements View { 13 | 14 | public ErrorView() { 15 | setCompositionRoot(new MVerticalLayout() 16 | .add(new MLabel("error - wrong url") 17 | .withFullWidth() 18 | .withStyleName(ValoTheme.NOTIFICATION_ERROR), Alignment.TOP_CENTER) 19 | .withSize(MSize.FULL_SIZE)); 20 | } 21 | 22 | @Override 23 | public void enter(ViewChangeEvent event) { 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/worker/ContractTerms.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.worker; 2 | 3 | import io.rocketbase.toggl.backend.model.global.Note; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.joda.time.LocalDate; 9 | 10 | import java.math.BigDecimal; 11 | import java.time.DayOfWeek; 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | @Data 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Builder 19 | public class ContractTerms { 20 | 21 | private String name; 22 | 23 | private Set weeklyWorkingDays; 24 | 25 | private BigDecimal weeklyWorkingHours; 26 | 27 | private int daysOfVacationPerYear; 28 | 29 | private BigDecimal grossMonthlySalary; 30 | 31 | private BigDecimal netMonthlySalary; 32 | 33 | private LocalDate validFrom; 34 | 35 | private LocalDate validTo; 36 | 37 | private List notes; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/tab/AbstractTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component.tab; 2 | 3 | import com.vaadin.ui.Component; 4 | import com.vaadin.ui.CustomComponent; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | /** 10 | * Created by marten on 30.01.17. 11 | */ 12 | public abstract class AbstractTab extends CustomComponent { 13 | 14 | @Setter(AccessLevel.PACKAGE) 15 | @Getter(AccessLevel.PROTECTED) 16 | private ExtendedTabSheet tabSheet; 17 | 18 | private boolean initialized = false; 19 | 20 | public AbstractTab() { 21 | setSizeFull(); 22 | } 23 | 24 | public abstract Component initLayout(); 25 | 26 | public abstract void onTabEnter(); 27 | 28 | protected void onEnter() { 29 | if (!initialized) { 30 | setCompositionRoot(initLayout()); 31 | initialized = true; 32 | } 33 | onTabEnter(); 34 | } 35 | 36 | protected void refreshTab() { 37 | tabSheet.triggerTabEnter(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/util/YearWeekUtil.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.util; 2 | 3 | import org.joda.time.LocalDate; 4 | import org.threeten.extra.YearWeek; 5 | 6 | import java.time.DayOfWeek; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public final class YearWeekUtil { 11 | 12 | 13 | public static LocalDate getFirstDay(YearWeek yearWeek) { 14 | return LocalDateConverter.convert(yearWeek.atDay(DayOfWeek.MONDAY)); 15 | } 16 | 17 | public static LocalDate getLastDay(YearWeek yearWeek) { 18 | return LocalDateConverter.convert(yearWeek.atDay(DayOfWeek.MONDAY)) 19 | .plusDays(6); 20 | } 21 | 22 | public static List getAllDatesOfYearWeek(YearWeek yearWeek) { 23 | List result = new ArrayList<>(); 24 | LocalDate start = getFirstDay(yearWeek); 25 | do { 26 | result.add(start); 27 | start = start.plusDays(1); 28 | } while (start.isBefore(getLastDay(yearWeek))); 29 | return result; 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/security/MongoUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.security; 2 | 3 | import io.rocketbase.toggl.backend.repository.MongoUserDetailsRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.Optional; 11 | 12 | @Service 13 | public class MongoUserDetailsService implements UserDetailsService { 14 | 15 | @Autowired 16 | private MongoUserDetailsRepository mongoUserDetailsRepository; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 | Optional userDetails = mongoUserDetailsRepository.findByUsername(username.toLowerCase()); 21 | MongoUserDetails user = userDetails.orElseThrow(() -> new UsernameNotFoundException("username not found")); 22 | return user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/DateTimeEntryGroup.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model; 2 | 3 | import io.rocketbase.toggl.api.model.TimeEntry; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.joda.time.DateTime; 9 | import org.joda.time.LocalDate; 10 | import org.springframework.data.annotation.Id; 11 | import org.springframework.data.mongodb.core.mapping.Document; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static io.rocketbase.toggl.backend.model.DateTimeEntryGroup.COLLECTION_NAME; 17 | 18 | 19 | /** 20 | * Created by marten on 08.03.17. 21 | */ 22 | @Document(collection = COLLECTION_NAME) 23 | @Data 24 | @AllArgsConstructor 25 | @NoArgsConstructor 26 | @Builder 27 | public class DateTimeEntryGroup { 28 | 29 | public static final String COLLECTION_NAME = "dateTimeEntryGroups"; 30 | 31 | @Id 32 | private String id; 33 | 34 | private long workspaceId; 35 | 36 | private LocalDate date; 37 | 38 | private DateTime fetched; 39 | 40 | private Map> userTimeEntriesMap; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/CustomViewAccessControl.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component; 2 | 3 | import com.vaadin.navigator.View; 4 | import com.vaadin.spring.access.ViewInstanceAccessControl; 5 | import com.vaadin.ui.UI; 6 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 7 | import io.rocketbase.toggl.ui.view.AbstractView; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.Resource; 13 | 14 | @Component 15 | public class CustomViewAccessControl implements ViewInstanceAccessControl { 16 | 17 | @Resource 18 | private ApplicationContext applicationContext; 19 | 20 | @Override 21 | public boolean isAccessGranted(UI ui, String beanName, View view) { 22 | Object principal = SecurityContextHolder.getContext() 23 | .getAuthentication() 24 | .getPrincipal(); 25 | 26 | if (principal instanceof MongoUserDetails) { 27 | AbstractView v = (AbstractView) applicationContext.getBean(view.getClass()); 28 | return ((MongoUserDetails) principal).getRole() 29 | .ordinal() >= v.getUserRole() 30 | .ordinal(); 31 | } 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.autoconfigure.web.ServerProperties; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | @Controller 12 | public class LoginController { 13 | 14 | @Autowired 15 | ServerProperties serverProperties; 16 | 17 | @RequestMapping(value = "/login", method = RequestMethod.GET) 18 | public ModelAndView login(@RequestParam(value = "error", required = false) String error, 19 | @RequestParam(value = "logged-out", required = false) String loggedOut) { 20 | ModelAndView modelAndView = new ModelAndView(); 21 | if (error != null) { 22 | modelAndView.addObject("error", true); 23 | } 24 | if (loggedOut != null) { 25 | modelAndView.addObject("loggedOut", true); 26 | } 27 | modelAndView.addObject("contextPath", serverProperties.getContextPath()); 28 | modelAndView.setViewName("login"); 29 | return modelAndView; 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/Worker.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model; 2 | 3 | import io.rocketbase.toggl.backend.model.global.Note; 4 | import io.rocketbase.toggl.backend.model.worker.Contact; 5 | import io.rocketbase.toggl.backend.model.worker.ContactType; 6 | import io.rocketbase.toggl.backend.model.worker.ContractTerms; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import org.joda.time.LocalDate; 12 | import org.springframework.data.annotation.Id; 13 | import org.springframework.data.annotation.Transient; 14 | import org.springframework.data.mongodb.core.mapping.Document; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import static io.rocketbase.toggl.backend.model.Worker.COLLECTION_NAME; 20 | 21 | @Document(collection = COLLECTION_NAME) 22 | @Data 23 | @AllArgsConstructor 24 | @NoArgsConstructor 25 | @Builder 26 | public class Worker { 27 | 28 | public static final String COLLECTION_NAME = "workers"; 29 | 30 | @Id 31 | private String id; 32 | 33 | private String firstName; 34 | 35 | private String lastName; 36 | 37 | private Map contacts; 38 | 39 | private LocalDate dateOfJoining; 40 | 41 | private List contractTerms; 42 | 43 | private List notes; 44 | 45 | @Transient 46 | public String getFullName() { 47 | return String.format("%s %s", firstName != null ? firstName : "", lastName != null ? lastName : "") 48 | .trim(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/AbstractView.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view; 2 | 3 | import com.vaadin.navigator.View; 4 | import com.vaadin.navigator.ViewChangeListener; 5 | import com.vaadin.server.FontIcon; 6 | import com.vaadin.ui.Component; 7 | import com.vaadin.ui.CustomComponent; 8 | import io.rocketbase.toggl.backend.security.UserRole; 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.Setter; 13 | 14 | /** 15 | * Created by marten on 08.03.17. 16 | */ 17 | @Getter 18 | @RequiredArgsConstructor 19 | public abstract class AbstractView extends CustomComponent implements View { 20 | 21 | private final String viewName; 22 | 23 | private final String caption; 24 | 25 | private final FontIcon icon; 26 | 27 | private final int order; 28 | 29 | protected boolean initialized = false; 30 | 31 | @Getter 32 | @Setter 33 | private boolean developmentMode = false; 34 | 35 | @Setter(AccessLevel.PROTECTED) 36 | private UserRole userRole = UserRole.ROLE_USER; 37 | 38 | @Override 39 | public void enter(ViewChangeListener.ViewChangeEvent viewChangeEvent) { 40 | if (!initialized) { 41 | setSizeFull(); 42 | setCompositionRoot(initialzeUi()); 43 | initialized = true; 44 | } 45 | } 46 | 47 | /** 48 | * initialize the ui - get fired only once after the first enter 49 | * 50 | * @return component that will get added to composition root 51 | */ 52 | public abstract Component initialzeUi(); 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/report/WeekTimeline.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.report; 2 | 3 | import de.jollyday.Holiday; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import org.threeten.extra.YearWeek; 7 | 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | @RequiredArgsConstructor 14 | @Getter 15 | public class WeekTimeline { 16 | 17 | private final YearWeek yearWeek; 18 | private Set holidays = new HashSet<>(); 19 | private Map uidTimelines = new HashMap<>(); 20 | 21 | private static double round(double value) { 22 | return Math.round((value) * 10.0) / 10.0; 23 | } 24 | 25 | public double getTotalHours() { 26 | return round(uidTimelines.values() 27 | .stream() 28 | .mapToDouble(e -> e.getWeekStatisticsOfWeek(yearWeek) 29 | .getTotalHours()) 30 | .sum()); 31 | } 32 | 33 | public double getBillableHours() { 34 | return round(uidTimelines.values() 35 | .stream() 36 | .mapToDouble(e -> e.getWeekStatisticsOfWeek(yearWeek) 37 | .getBillableHours()) 38 | .sum()); 39 | } 40 | 41 | public long getBillableAmount() { 42 | return uidTimelines.values() 43 | .stream() 44 | .mapToLong(e -> e.getWeekStatisticsOfWeek(yearWeek) 45 | .getBillableAmount()) 46 | .sum(); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/security/MongoUserDetails.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.security; 2 | 3 | import io.rocketbase.toggl.backend.model.Worker; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.mongodb.core.mapping.DBRef; 10 | import org.springframework.data.mongodb.core.mapping.Document; 11 | import org.springframework.security.core.GrantedAuthority; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | 14 | import javax.validation.constraints.NotNull; 15 | import java.util.Arrays; 16 | import java.util.Collection; 17 | 18 | @Document(collection = "users") 19 | @Data 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | @Builder 23 | public class MongoUserDetails implements UserDetails { 24 | 25 | @Id 26 | private String id; 27 | 28 | @NotNull 29 | private String username; 30 | 31 | @NotNull 32 | private String password; 33 | 34 | private UserRole role; 35 | 36 | @DBRef 37 | private Worker worker; 38 | 39 | @Builder.Default 40 | private boolean enabled = true; 41 | 42 | @Override 43 | public Collection getAuthorities() { 44 | return Arrays.asList(role); 45 | } 46 | 47 | @Override 48 | public boolean isAccountNonExpired() { 49 | return true; 50 | } 51 | 52 | @Override 53 | public boolean isAccountNonLocked() { 54 | return true; 55 | } 56 | 57 | @Override 58 | public boolean isCredentialsNonExpired() { 59 | return true; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/util/YearMonthUtil.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.util; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import org.joda.time.LocalDate; 7 | import org.joda.time.YearMonth; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Comparator; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Created by marten on 09.03.17. 16 | */ 17 | public final class YearMonthUtil { 18 | 19 | public static List getAllDatesOfMonth(YearMonth yearMonth) { 20 | List result = new ArrayList<>(); 21 | org.joda.time.LocalDate start = yearMonth.toLocalDate(1); 22 | do { 23 | result.add(start); 24 | start = start.plusDays(1); 25 | } while (start.getMonthOfYear() == yearMonth.getMonthOfYear()); 26 | return result; 27 | } 28 | 29 | public static List getAllWeeksOfWeekyear(YearMonth yearMonth) { 30 | List result = getAllDatesOfMonth(yearMonth); 31 | return result.stream() 32 | .map(d -> new SortedWeek(d.getDayOfYear(), d.getWeekOfWeekyear())) 33 | .collect(Collectors.toSet()) 34 | .stream() 35 | .sorted(Comparator.comparing(SortedWeek::getSorting)) 36 | .map(v -> v.getWeek()) 37 | .collect(Collectors.toList()); 38 | } 39 | 40 | @Getter 41 | @EqualsAndHashCode(of = {"week"}) 42 | @RequiredArgsConstructor 43 | private static class SortedWeek { 44 | 45 | private final int sorting; 46 | private final int week; 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/scheduler/PullTimeEntriesScheduler.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.scheduler; 2 | 3 | import io.rocketbase.toggl.backend.config.TogglService; 4 | import io.rocketbase.toggl.backend.model.ApplicationSetting.SchedulingConfig; 5 | import io.rocketbase.toggl.backend.service.FetchAndStoreService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.joda.time.LocalDate; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Service 14 | @Slf4j 15 | public class PullTimeEntriesScheduler { 16 | 17 | @Resource 18 | private TogglService togglService; 19 | 20 | @Resource 21 | private FetchAndStoreService fetchAndStoreService; 22 | 23 | // each hour, 30sec initial delay 24 | @Scheduled(fixedDelay = 60 * 60 * 1000, initialDelay = 1000 * 30) 25 | public void schedule() { 26 | SchedulingConfig schedulingConfig = togglService.getSchedulingConfig(); 27 | if (schedulingConfig != null && schedulingConfig.isEnableScheduling()) { 28 | log.info("start scheduler"); 29 | long startTime = System.currentTimeMillis(); 30 | LocalDate start = schedulingConfig.getLastFinishedDate() != null ? schedulingConfig.getLastFinishedDate() : schedulingConfig.getStartSchedulingFrom(); 31 | 32 | fetchAndStoreService.fetchBetween(start, LocalDate.now()); 33 | 34 | schedulingConfig.setLastFinishedDate(LocalDate.now()); 35 | togglService.updateSchedulingConfig(schedulingConfig); 36 | log.info("finished scheduling - took: {} ms", System.currentTimeMillis() - startTime); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/LeaveEntry.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model; 2 | 3 | import io.rocketbase.toggl.backend.model.global.Note; 4 | import io.rocketbase.toggl.backend.model.leave.LeaveStatus; 5 | import io.rocketbase.toggl.backend.model.leave.LeaveType; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import org.joda.time.LocalDate; 11 | import org.springframework.data.annotation.Id; 12 | import org.springframework.data.mongodb.core.mapping.DBRef; 13 | import org.springframework.data.mongodb.core.mapping.Document; 14 | 15 | import java.util.List; 16 | 17 | import static io.rocketbase.toggl.backend.model.LeaveEntry.COLLECTION_NAME; 18 | 19 | @Document(collection = COLLECTION_NAME) 20 | @Data 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | @Builder 24 | public class LeaveEntry { 25 | 26 | public static final String COLLECTION_NAME = "leaveEntries"; 27 | 28 | @Id 29 | private String id; 30 | 31 | @DBRef 32 | private Worker worker; 33 | 34 | private LeaveStatus leaveStatus; 35 | 36 | private LeaveType leaveType; 37 | 38 | private LocalDate start; 39 | 40 | private LocalDate end; 41 | 42 | /** 43 | * dates that will get counted/reduce vaction (this skipps weekend and holidays) 44 | */ 45 | private List clearedDays; 46 | 47 | /** 48 | * logs status-changes 49 | */ 50 | private List protocols; 51 | 52 | @Data 53 | @AllArgsConstructor 54 | @NoArgsConstructor 55 | @Builder 56 | private static class LeaveProtocol { 57 | 58 | private LeaveStatus status; 59 | 60 | private Note note; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/DailyWorkingLog.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.joda.time.LocalDate; 9 | import org.springframework.data.annotation.Id; 10 | import org.springframework.data.mongodb.core.mapping.DBRef; 11 | import org.springframework.data.mongodb.core.mapping.Document; 12 | 13 | import java.math.BigDecimal; 14 | 15 | import static io.rocketbase.toggl.backend.model.DailyWorkingLog.COLLECTION_NAME; 16 | 17 | @Document(collection = COLLECTION_NAME) 18 | @Data 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | @Builder 22 | public class DailyWorkingLog { 23 | 24 | public static final String COLLECTION_NAME = "dailyWorkingLogs"; 25 | 26 | @Id 27 | private String id; 28 | 29 | @DBRef(lazy = true) 30 | private Worker worker; 31 | 32 | @DBRef(lazy = true) 33 | private LeaveEntry leaveEntry; 34 | 35 | private LocalDate date; 36 | 37 | /** 38 | * logs the time someone should work 39 | */ 40 | private Integer minutesToWork; 41 | 42 | /** 43 | * actual worked minutes by someone 44 | */ 45 | private Integer minutesWorked; 46 | 47 | /** 48 | * time to used in daily calculation
49 | * for example someone worked 3hrs and then stopped earlier because of sickness -> daily working hours get filled, but minutesWorked remains by 3hrs 50 | */ 51 | private Integer minutesLogged; 52 | 53 | /** 54 | * actual earned money by worker 55 | */ 56 | private BigDecimal moneyEarned; 57 | 58 | /** 59 | * automatically update will get stopped when locked=true 60 | */ 61 | private boolean locked; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toggl-reporter 2 | 3 | ![screenshot](assets/screencast.gif) 4 | 5 | Tiny SpringBoot-Application, build with vaadin and mongodb, that pulls your TimeEntries of your workspace from [toggl](https://toggl.com). 6 | The stored information allow a fine grained reporting and analysis that aren't possible within toggl. 7 | Some Report examples: 8 | * Daily working hours of each team-member in comparison 9 | * Worked hours within week per each member 10 | * earned money, average hours per day etc. 11 | 12 | Why we've build this app: As a small software mill we need to track your employee's time and perform some checks. Furthermore detailed reportings and performance indicators encourage everybody within the team. 13 | 14 | ## usage 15 | 16 | Mainly we've designed the application to run within docker. We've provided an image [rocketbaseio/toggl-reporter](https://hub.docker.com/r/rocketbaseio/toggl-reporter/). 17 | 18 | ```shell 19 | # shell command to get it run within docker 20 | docker run -ti --rm -e SPRING_DATA_MONGODB_URI=mongodb://mongo/toggl-report -p 8080:8080 --link mongo:mongo rocketbaseio/toggl-reporter 21 | # link the application to a mongo-container and configure datebase settings 22 | ``` 23 | 24 | At first run an **admin** user with password **admin** wil get created. After login the application ask's for an toggl-api-token. All configurations, tokens etc. will get stored to your mongodb. 25 | 26 | ***It's recommended to change the password of the admin-user!*** 27 | 28 | ## configuration 29 | 30 | * SPRING_DATA_MONGODB_URI *(for example mongodb://mongo/toggl-report)* 31 | * APPLICATION_TITLE *(will get displayed on login screen and header or menu)* 32 | * default spring boot parameters like port etc. 33 | 34 | ## remarks 35 | 36 | this is an initial version - no warranties etc... 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/tab/ExtendedTabSheet.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component.tab; 2 | 3 | import com.vaadin.server.FontIcon; 4 | import com.vaadin.ui.CustomComponent; 5 | import com.vaadin.ui.TabSheet; 6 | import com.vaadin.ui.TabSheet.Tab; 7 | import com.vaadin.ui.themes.ValoTheme; 8 | 9 | 10 | public class ExtendedTabSheet extends CustomComponent { 11 | 12 | private TabSheet tabSheet; 13 | private AbstractTab currentTab; 14 | private T filter; 15 | 16 | public ExtendedTabSheet() { 17 | setSizeFull(); 18 | 19 | initTabSheet(); 20 | setCompositionRoot(tabSheet); 21 | addAttachListener(event -> { 22 | triggerTabEnter(); 23 | }); 24 | } 25 | 26 | private void initTabSheet() { 27 | tabSheet = new TabSheet(); 28 | tabSheet.setSizeFull(); 29 | tabSheet.addStyleName(ValoTheme.TABSHEET_COMPACT_TABBAR); 30 | tabSheet.addSelectedTabChangeListener(event -> { 31 | currentTab = (AbstractTab) event.getTabSheet() 32 | .getSelectedTab(); 33 | triggerTabEnter(); 34 | }); 35 | } 36 | 37 | void triggerTabEnter() { 38 | if (currentTab != null) { 39 | currentTab.onEnter(); 40 | } 41 | } 42 | 43 | public ExtendedTabSheet addTab(FontIcon icon, String caption, AbstractTab tab) { 44 | tab.setTabSheet(this); 45 | Tab t = tabSheet.addTab(tab, caption); 46 | t.setIcon(icon); 47 | if (currentTab == null) { 48 | currentTab = tab; 49 | } 50 | return this; 51 | } 52 | 53 | public T getFilter() { 54 | return filter; 55 | } 56 | 57 | public void setFilterAndRefresh(T filter) { 58 | this.filter = filter; 59 | triggerTabEnter(); 60 | } 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/service/HolidayManagerService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.service; 2 | 3 | import de.jollyday.Holiday; 4 | import de.jollyday.HolidayCalendar; 5 | import de.jollyday.HolidayManager; 6 | import de.jollyday.ManagerParameters; 7 | import io.rocketbase.toggl.backend.config.TogglService; 8 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 9 | import org.joda.time.YearMonth; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.annotation.Resource; 13 | import java.time.LocalDate; 14 | import java.util.Set; 15 | import java.util.TreeSet; 16 | 17 | /** 18 | * Created by marten on 10.03.17. 19 | */ 20 | @Service 21 | public class HolidayManagerService { 22 | 23 | @Resource 24 | private TogglService togglService; 25 | 26 | 27 | public Set getHolidays(YearMonth yearMonth) { 28 | HolidayCalendar holidayCalendar = togglService.getHolidayCalender(); 29 | Set result = new TreeSet<>(); 30 | if (holidayCalendar != null) { 31 | HolidayManager m = HolidayManager.getInstance(ManagerParameters.create(holidayCalendar, null)); 32 | result.addAll(m.getHolidays(LocalDateConverter.convert(yearMonth.toLocalDate(1)), 33 | LocalDateConverter.convert(yearMonth.toLocalDate(1) 34 | .plusMonths(1) 35 | .minusDays(1)))); 36 | } 37 | return result; 38 | } 39 | 40 | public Set getHolidays(LocalDate from, LocalDate to) { 41 | HolidayCalendar holidayCalendar = togglService.getHolidayCalender(); 42 | Set result = new TreeSet<>(); 43 | if (holidayCalendar != null) { 44 | HolidayManager m = HolidayManager.getInstance(ManagerParameters.create(holidayCalendar, null)); 45 | result.addAll(m.getHolidays(from, to)); 46 | } 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/window/LinkWorkerWindow.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.window; 2 | 3 | 4 | import com.vaadin.ui.ComboBox; 5 | import com.vaadin.ui.Window; 6 | import io.rocketbase.toggl.backend.model.Worker; 7 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 8 | import io.rocketbase.toggl.backend.security.MongoUserService; 9 | import io.rocketbase.toggl.backend.service.WorkerService; 10 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 11 | import org.springframework.context.annotation.Scope; 12 | import org.vaadin.viritin.button.PrimaryButton; 13 | import org.vaadin.viritin.layouts.MVerticalLayout; 14 | 15 | import javax.annotation.Resource; 16 | 17 | @org.springframework.stereotype.Component 18 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 19 | public class LinkWorkerWindow extends Window { 20 | 21 | @Resource 22 | private WorkerService workerService; 23 | 24 | @Resource 25 | private MongoUserService mongoUserService; 26 | 27 | public LinkWorkerWindow linkUser(MongoUserDetails userDetails) { 28 | setCaption(String.format("link user: %s", userDetails.getUsername())); 29 | setWidth("400px"); 30 | setModal(true); 31 | setResizable(false); 32 | setDraggable(false); 33 | center(); 34 | 35 | ComboBox workerSelect = new ComboBox<>("Worker", workerService.findAll()); 36 | workerSelect.setItemCaptionGenerator(e -> e.getFullName()); 37 | workerSelect.setWidth("100%"); 38 | if (userDetails.getWorker() != null) { 39 | workerSelect.setValue(userDetails.getWorker()); 40 | } 41 | 42 | setContent(new MVerticalLayout() 43 | .add(workerSelect) 44 | .add(new PrimaryButton("Save", e -> { 45 | mongoUserService.updateWorker(userDetails, workerSelect.getValue()); 46 | close(); 47 | })) 48 | .withFullWidth()); 49 | 50 | return this; 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/ApplicationSetting.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model; 2 | 3 | import ch.simas.jtoggl.domain.Workspace; 4 | import de.jollyday.HolidayCalendar; 5 | import io.rocketbase.toggl.backend.util.ColorPalette; 6 | import lombok.*; 7 | import org.joda.time.LocalDate; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.mongodb.core.mapping.Document; 10 | 11 | import java.time.DayOfWeek; 12 | import java.util.Map; 13 | import java.util.Set; 14 | 15 | import static io.rocketbase.toggl.backend.model.ApplicationSetting.COLLECTION_NAME; 16 | 17 | /** 18 | * Created by marten on 08.03.17. 19 | */ 20 | @Document(collection = COLLECTION_NAME) 21 | @Data 22 | @AllArgsConstructor 23 | @NoArgsConstructor 24 | @Builder 25 | public class ApplicationSetting { 26 | 27 | public static final String COLLECTION_NAME = "applicationSettings"; 28 | 29 | @Id 30 | private String id; 31 | 32 | private long currentWorkspaceId; 33 | 34 | private String apiToken; 35 | 36 | private Map workspaceMap; 37 | 38 | private Map userMap; 39 | 40 | private HolidayCalendar holidayCalendar; 41 | 42 | private Set regularWorkinsDays; 43 | 44 | private SchedulingConfig schedulingConfig = SchedulingConfig.EMPTY; 45 | 46 | @Data 47 | @RequiredArgsConstructor 48 | public static class UserDetails { 49 | 50 | private final long uid; 51 | 52 | private final String name; 53 | 54 | private final String email; 55 | 56 | private ColorPalette graphColor; 57 | 58 | private String avatar; 59 | 60 | } 61 | 62 | 63 | @Data 64 | @RequiredArgsConstructor 65 | public static class SchedulingConfig { 66 | 67 | public static final SchedulingConfig EMPTY = new SchedulingConfig(false, null); 68 | 69 | private final boolean enableScheduling; 70 | 71 | private final LocalDate startSchedulingFrom; 72 | 73 | private LocalDate lastFinishedDate; 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/SettingView.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting; 2 | 3 | import com.vaadin.icons.VaadinIcons; 4 | import com.vaadin.spring.annotation.SpringView; 5 | import com.vaadin.spring.annotation.UIScope; 6 | import com.vaadin.ui.Component; 7 | import io.rocketbase.toggl.backend.security.UserRole; 8 | import io.rocketbase.toggl.ui.component.tab.ExtendedTabSheet; 9 | import io.rocketbase.toggl.ui.view.AbstractView; 10 | import io.rocketbase.toggl.ui.view.setting.tab.LoginUserTab; 11 | import io.rocketbase.toggl.ui.view.setting.tab.PullDataTab; 12 | import io.rocketbase.toggl.ui.view.setting.tab.SchedulingTab; 13 | import io.rocketbase.toggl.ui.view.setting.tab.SettingTab; 14 | import org.vaadin.viritin.MSize; 15 | import org.vaadin.viritin.layouts.MVerticalLayout; 16 | 17 | import javax.annotation.Resource; 18 | 19 | /** 20 | * Created by marten on 08.03.17. 21 | */ 22 | @UIScope 23 | @SpringView(name = SettingView.VIEW_NAME) 24 | public class SettingView extends AbstractView { 25 | 26 | public static final String VIEW_NAME = "setting"; 27 | 28 | 29 | @Resource 30 | private SettingTab settingTab; 31 | 32 | @Resource 33 | private SchedulingTab schedulingTab; 34 | 35 | @Resource 36 | private LoginUserTab loginUserTab; 37 | 38 | @Resource 39 | private PullDataTab pullDataTab; 40 | 41 | public SettingView() { 42 | super(VIEW_NAME, "Setting", VaadinIcons.WRENCH, 100); 43 | setUserRole(UserRole.ROLE_ADMIN); 44 | } 45 | 46 | @Override 47 | public Component initialzeUi() { 48 | ExtendedTabSheet tabSheet = new ExtendedTabSheet(); 49 | tabSheet.addTab(VaadinIcons.WRENCH, "settings", settingTab); 50 | tabSheet.addTab(VaadinIcons.CALENDAR_CLOCK, "scheduling", schedulingTab); 51 | tabSheet.addTab(VaadinIcons.USERS, "login-user", loginUserTab); 52 | tabSheet.addTab(VaadinIcons.DOWNLOAD, "pull-data", pullDataTab); 53 | 54 | 55 | return new MVerticalLayout() 56 | .add(tabSheet, 1) 57 | .withSize(MSize.FULL_SIZE); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/MainScreen.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component; 2 | 3 | import com.vaadin.navigator.Navigator; 4 | import com.vaadin.navigator.ViewChangeListener; 5 | import com.vaadin.spring.annotation.SpringComponent; 6 | import com.vaadin.spring.annotation.UIScope; 7 | import com.vaadin.spring.navigator.SpringViewProvider; 8 | import com.vaadin.ui.CssLayout; 9 | import com.vaadin.ui.HorizontalLayout; 10 | import com.vaadin.ui.UI; 11 | import io.rocketbase.toggl.ui.view.error.ErrorView; 12 | import org.springframework.beans.factory.annotation.Value; 13 | 14 | import javax.annotation.Resource; 15 | 16 | /** 17 | * Created by marten on 08.03.17. 18 | */ 19 | @UIScope 20 | @SpringComponent 21 | public class MainScreen extends HorizontalLayout { 22 | 23 | @Value("${application.title}") 24 | private String applicationTitle; 25 | 26 | @Resource 27 | private SpringViewProvider viewProvider; 28 | 29 | @Resource 30 | private Menu menu; 31 | 32 | private CssLayout viewContainer; 33 | 34 | public MainScreen initWithUi(UI ui) { 35 | setStyleName("main-screen"); 36 | 37 | viewContainer = new CssLayout(); 38 | viewContainer.addStyleName("valo-content"); 39 | viewContainer.setSizeFull(); 40 | 41 | 42 | Navigator navigator = new Navigator(ui, viewContainer); 43 | navigator.addProvider(viewProvider); 44 | navigator.setErrorView(ErrorView.class); 45 | navigator.addViewChangeListener(new ViewChangeListener() { 46 | 47 | @Override 48 | public boolean beforeViewChange(ViewChangeEvent event) { 49 | return true; 50 | } 51 | 52 | @Override 53 | public void afterViewChange(ViewChangeEvent event) { 54 | menu.setActiveView(event.getViewName()); 55 | } 56 | 57 | }); 58 | 59 | ui.getPage() 60 | .setTitle(applicationTitle); 61 | 62 | addComponent(menu); 63 | addComponent(viewContainer); 64 | setExpandRatio(viewContainer, 1); 65 | setSizeFull(); 66 | 67 | return this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/home/HomeView.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.home; 2 | 3 | import com.vaadin.icons.VaadinIcons; 4 | import com.vaadin.navigator.ViewChangeListener; 5 | import com.vaadin.spring.annotation.SpringView; 6 | import com.vaadin.spring.annotation.UIScope; 7 | import com.vaadin.ui.Alignment; 8 | import com.vaadin.ui.Component; 9 | import io.rocketbase.toggl.ui.component.tab.ExtendedTabSheet; 10 | import io.rocketbase.toggl.ui.view.AbstractView; 11 | import io.rocketbase.toggl.ui.view.home.tab.ChartTab; 12 | import io.rocketbase.toggl.ui.view.home.tab.MonthStatisticsTab; 13 | import io.rocketbase.toggl.ui.view.home.tab.WeekStatisticsTab; 14 | import org.vaadin.viritin.MSize; 15 | import org.vaadin.viritin.label.MLabel; 16 | import org.vaadin.viritin.layouts.MVerticalLayout; 17 | 18 | import javax.annotation.Resource; 19 | 20 | /** 21 | * Created by marten on 08.03.17. 22 | */ 23 | @UIScope 24 | @SpringView(name = HomeView.VIEW_NAME) 25 | public class HomeView extends AbstractView { 26 | 27 | public static final String VIEW_NAME = ""; 28 | 29 | @Resource 30 | private ChartTab chartTab; 31 | 32 | @Resource 33 | private MonthStatisticsTab monthStatisticsTab; 34 | 35 | @Resource 36 | private WeekStatisticsTab weekStatisticsTab; 37 | 38 | public HomeView() { 39 | super(VIEW_NAME, "Chart", VaadinIcons.LINE_CHART, 0); 40 | } 41 | 42 | public static Component getPlaceHolder() { 43 | return new MVerticalLayout() 44 | .add(new MLabel("select year month").withStyleName("text-center", "placeholder"), Alignment.MIDDLE_CENTER) 45 | .withSize(MSize.FULL_SIZE); 46 | } 47 | 48 | @Override 49 | public Component initialzeUi() { 50 | ExtendedTabSheet tabSheet = new ExtendedTabSheet(); 51 | tabSheet.addTab(VaadinIcons.LINE_CHART, "chart", chartTab); 52 | tabSheet.addTab(VaadinIcons.STAR_O, "month-statistics", monthStatisticsTab); 53 | tabSheet.addTab(VaadinIcons.CALENDAR, "week-statistics", weekStatisticsTab); 54 | 55 | 56 | return new MVerticalLayout() 57 | .add(tabSheet, 1) 58 | .withSize(MSize.FULL_SIZE); 59 | } 60 | 61 | @Override 62 | public void enter(ViewChangeListener.ViewChangeEvent viewChangeEvent) { 63 | super.enter(viewChangeEvent); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/worker/form/WorkerForm.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.worker.form; 2 | 3 | import com.vaadin.ui.Component; 4 | import com.vaadin.ui.DateField; 5 | import com.vaadin.ui.TextField; 6 | import com.vaadin.ui.Window; 7 | import io.rocketbase.toggl.backend.model.Worker; 8 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 9 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 10 | import org.springframework.context.annotation.Scope; 11 | import org.vaadin.viritin.fields.MTextField; 12 | import org.vaadin.viritin.form.AbstractForm; 13 | import org.vaadin.viritin.layouts.MVerticalLayout; 14 | 15 | @org.springframework.stereotype.Component 16 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 17 | public class WorkerForm extends AbstractForm { 18 | 19 | private TextField firstName = new MTextField("first name").withFullWidth(); 20 | 21 | private TextField lastName = new MTextField("last name").withFullWidth(); 22 | 23 | private DateField dateOfJoiningJavaTime = new DateField(); 24 | 25 | public WorkerForm() { 26 | super(Worker.class); 27 | 28 | getBinder().bind(firstName, "firstName"); 29 | getBinder().bind(lastName, "lastName"); 30 | } 31 | 32 | @Override 33 | public Worker getEntity() { 34 | Worker worker = super.getEntity(); 35 | // manual mapping because of joda java.time conversation problems 36 | worker.setDateOfJoining(dateOfJoiningJavaTime.getValue() != null ? LocalDateConverter.convert(dateOfJoiningJavaTime.getValue()) : null); 37 | return worker; 38 | } 39 | 40 | @Override 41 | public void setEntity(Worker entity) { 42 | super.setEntity(entity); 43 | if (entity != null && entity.getDateOfJoining() != null) { 44 | dateOfJoiningJavaTime.setValue(LocalDateConverter.convert(entity.getDateOfJoining())); 45 | } 46 | } 47 | 48 | @Override 49 | public Window openInModalPopup() { 50 | Window window = super.openInModalPopup(); 51 | window.setWidth("500px"); 52 | return window; 53 | } 54 | 55 | @Override 56 | protected Component createContent() { 57 | dateOfJoiningJavaTime.setWidth("100%"); 58 | return new MVerticalLayout() 59 | .add(firstName) 60 | .add(lastName) 61 | .add(dateOfJoiningJavaTime) 62 | .add(getToolbar()) 63 | .withFullWidth(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/MainUI.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui; 2 | 3 | import com.vaadin.annotations.StyleSheet; 4 | import com.vaadin.server.FontAwesome; 5 | import com.vaadin.server.Responsive; 6 | import com.vaadin.server.VaadinRequest; 7 | import com.vaadin.spring.annotation.SpringUI; 8 | import com.vaadin.ui.Alignment; 9 | import com.vaadin.ui.Notification; 10 | import com.vaadin.ui.UI; 11 | import com.vaadin.ui.themes.ValoTheme; 12 | import io.rocketbase.toggl.backend.config.TogglService; 13 | import io.rocketbase.toggl.ui.component.MainScreen; 14 | import org.vaadin.viritin.button.MButton; 15 | import org.vaadin.viritin.fields.MTextField; 16 | import org.vaadin.viritin.layouts.MVerticalLayout; 17 | import org.vaadin.viritin.layouts.MWindow; 18 | 19 | import javax.annotation.Resource; 20 | 21 | /** 22 | * Created by marten on 20.02.17. 23 | */ 24 | @SpringUI(path = "/app") 25 | @StyleSheet("design.css") 26 | public class MainUI extends UI { 27 | 28 | @Resource 29 | private MainScreen mainScreen; 30 | 31 | @Resource 32 | private TogglService togglService; 33 | 34 | @Override 35 | protected void init(VaadinRequest vaadinRequest) { 36 | Responsive.makeResponsive(this); 37 | setLocale(vaadinRequest.getLocale()); 38 | 39 | addStyleName(ValoTheme.UI_WITH_MENU); 40 | 41 | if (!togglService.isApiTokenAvailable()) { 42 | initTokenWizard(); 43 | 44 | } else { 45 | setContent(mainScreen.initWithUi(this)); 46 | } 47 | 48 | } 49 | 50 | private void initTokenWizard() { 51 | MTextField apiToken = new MTextField("Api-Token").withFullWidth(); 52 | 53 | MWindow configWindow = new MWindow("Configure API-Token") 54 | .withWidth("50%") 55 | .withModal(true) 56 | .withResizable(false) 57 | .withDraggable(false) 58 | .withClosable(false) 59 | .withCenter(); 60 | 61 | configWindow.setContent(new MVerticalLayout() 62 | .add(apiToken, Alignment.MIDDLE_CENTER, 1) 63 | .add(new MButton(FontAwesome.SAVE, "Save", e -> { 64 | try { 65 | togglService.updateToken(apiToken.getValue()); 66 | 67 | configWindow.close(); 68 | setContent(mainScreen.initWithUi(this)); 69 | } catch (Exception exp) { 70 | Notification.show("invalid api-token", Notification.Type.ERROR_MESSAGE); 71 | } 72 | }), Alignment.MIDDLE_CENTER)); 73 | 74 | UI.getCurrent() 75 | .addWindow(configWindow); 76 | 77 | setContent(new MVerticalLayout()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/tab/SchedulingTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.tab; 2 | 3 | import com.vaadin.spring.annotation.SpringComponent; 4 | import com.vaadin.spring.annotation.UIScope; 5 | import com.vaadin.ui.CheckBox; 6 | import com.vaadin.ui.Component; 7 | import com.vaadin.ui.DateField; 8 | import io.rocketbase.toggl.backend.config.TogglService; 9 | import io.rocketbase.toggl.backend.model.ApplicationSetting.SchedulingConfig; 10 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 11 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 12 | import org.vaadin.viritin.button.PrimaryButton; 13 | import org.vaadin.viritin.label.RichText; 14 | import org.vaadin.viritin.layouts.MVerticalLayout; 15 | 16 | import javax.annotation.Resource; 17 | 18 | /** 19 | * Created by marten on 09.03.17. 20 | */ 21 | @UIScope 22 | @SpringComponent 23 | public class SchedulingTab extends AbstractTab { 24 | 25 | @Resource 26 | private TogglService togglService; 27 | 28 | private CheckBox enableScheduling; 29 | 30 | private DateField startSchedulingFrom; 31 | 32 | @Override 33 | public Component initLayout() { 34 | enableScheduling = new CheckBox("enable scheduling", false); 35 | enableScheduling.addValueChangeListener(e -> checkStatus()); 36 | 37 | startSchedulingFrom = new DateField("scheduling start from"); 38 | checkStatus(); 39 | 40 | return new MVerticalLayout() 41 | .add(new RichText().withMarkDown("### explanation\n" + 42 | "will schedule every hour and refresh's not finished days") 43 | .withFullWidth()) 44 | .add(enableScheduling) 45 | .add(startSchedulingFrom) 46 | .add(new PrimaryButton("Save", event -> { 47 | togglService.updateSchedulingConfig(new SchedulingConfig(enableScheduling.getValue(), 48 | LocalDateConverter.convert(startSchedulingFrom.getValue()))); 49 | })) 50 | .withFullWidth(); 51 | } 52 | 53 | private void checkStatus() { 54 | startSchedulingFrom.setVisible(enableScheduling.getValue()); 55 | } 56 | 57 | @Override 58 | public void onTabEnter() { 59 | SchedulingConfig schedulingConfig = togglService.getSchedulingConfig(); 60 | if (schedulingConfig != null) { 61 | enableScheduling.setValue(schedulingConfig 62 | .isEnableScheduling()); 63 | 64 | startSchedulingFrom.setValue(schedulingConfig 65 | .getStartSchedulingFrom() != null ? 66 | LocalDateConverter.convert( 67 | schedulingConfig 68 | .getStartSchedulingFrom()) : null); 69 | } 70 | checkStatus(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.config; 2 | 3 | import io.rocketbase.toggl.backend.security.MongoUserDetailsService; 4 | import io.rocketbase.toggl.backend.security.MongoUserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.autoconfigure.security.SecurityProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 10 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | 16 | import javax.annotation.Resource; 17 | 18 | @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 19 | @Configuration 20 | @EnableWebSecurity 21 | @EnableGlobalMethodSecurity(securedEnabled = true) 22 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 23 | 24 | @Autowired 25 | private MongoUserDetailsService userDetailsService; 26 | 27 | @Resource 28 | private MongoUserService mongoUserService; 29 | 30 | @Override 31 | public void configure(AuthenticationManagerBuilder auth) throws Exception { 32 | auth 33 | .userDetailsService(userDetailsService) 34 | .passwordEncoder(new BCryptPasswordEncoder()); 35 | 36 | mongoUserService.checkInitialState(); 37 | } 38 | 39 | @Override 40 | protected void configure(HttpSecurity http) throws Exception { 41 | http 42 | .csrf() 43 | .disable() // Use Vaadin's CSRF protection 44 | .authorizeRequests() 45 | .antMatchers("/favicon.ico") 46 | .permitAll() 47 | .anyRequest() 48 | .authenticated() // User must be authenticated to access any part of the application 49 | .and() 50 | .formLogin() 51 | .loginPage("/login") 52 | .failureUrl("/login?error") 53 | .defaultSuccessUrl("/app", true) 54 | .permitAll() // Login page is accessible to anybody 55 | .and() 56 | .rememberMe() 57 | .rememberMeParameter("remember-me") 58 | .tokenValiditySeconds(86400 * 7) 59 | .and() 60 | .logout() 61 | .logoutUrl("/logout") 62 | .logoutSuccessUrl("/login?logout") 63 | .permitAll() // Logout success page is accessible to anybody 64 | .and() 65 | .sessionManagement() 66 | .sessionFixation() 67 | .newSession(); // Create completely new session 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/security/MongoUserService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.security; 2 | 3 | import io.rocketbase.toggl.backend.model.Worker; 4 | import io.rocketbase.toggl.backend.repository.MongoUserDetailsRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.BeanUtils; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.annotation.Resource; 13 | import java.util.List; 14 | 15 | @Slf4j 16 | @Service 17 | public class MongoUserService { 18 | 19 | @Resource 20 | private MongoUserDetailsRepository repository; 21 | 22 | private PasswordEncoder encoder = new BCryptPasswordEncoder(); 23 | 24 | public MongoUserDetails register(MongoUserDetails userDetails) { 25 | String username = userDetails.getUsername() 26 | .toLowerCase(); 27 | if (repository.findByUsername(username) 28 | .isPresent()) { 29 | throw new RuntimeException("username already in use"); 30 | } 31 | userDetails.setUsername(username); 32 | userDetails.setPassword(encoder.encode(userDetails.getPassword())); 33 | return repository.save(userDetails); 34 | } 35 | 36 | public void checkInitialState() { 37 | if (repository.count() == 0) { 38 | repository.save(MongoUserDetails.builder() 39 | .enabled(true) 40 | .password(encoder.encode("admin")) 41 | .role(UserRole.ROLE_ADMIN) 42 | .username("admin") 43 | .build()); 44 | log.info("initialized admin user"); 45 | } 46 | } 47 | 48 | public List findAll() { 49 | return repository.findAll(); 50 | } 51 | 52 | public MongoUserDetails updateDetailsExceptPassword(MongoUserDetails entity) { 53 | MongoUserDetails dbEntity = repository.findOne(entity.getId()); 54 | BeanUtils.copyProperties(entity, dbEntity, "password"); 55 | return repository.save(dbEntity); 56 | } 57 | 58 | public void delete(MongoUserDetails entity) { 59 | Object p = SecurityContextHolder.getContext() 60 | .getAuthentication() 61 | .getPrincipal(); 62 | if (p instanceof MongoUserDetails && ((MongoUserDetails) p).getUsername() 63 | .equals(entity.getUsername())) { 64 | throw new RuntimeException("u cannot delete yourself!"); 65 | } 66 | repository.delete(entity); 67 | } 68 | 69 | public MongoUserDetails updatePassword(MongoUserDetails user, String newPassword) { 70 | MongoUserDetails dbEntity = repository.findByUsername(user.getUsername()) 71 | .get(); 72 | dbEntity.setPassword(encoder.encode(newPassword)); 73 | return repository.save(dbEntity); 74 | } 75 | 76 | public MongoUserDetails updateWorker(MongoUserDetails user, Worker worker) { 77 | MongoUserDetails dbEntity = repository.findByUsername(user.getUsername()) 78 | .get(); 79 | dbEntity.setWorker(worker); 80 | return repository.save(dbEntity); 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/NoteComponent.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component; 2 | 3 | import com.vaadin.server.FontAwesome; 4 | import com.vaadin.ui.*; 5 | import com.vaadin.ui.themes.ValoTheme; 6 | import io.rocketbase.toggl.backend.model.global.Note; 7 | import org.joda.time.format.DateTimeFormat; 8 | import org.vaadin.viritin.MSize; 9 | import org.vaadin.viritin.button.MButton; 10 | import org.vaadin.viritin.fields.MTextField; 11 | import org.vaadin.viritin.label.MLabel; 12 | import org.vaadin.viritin.label.RichText; 13 | import org.vaadin.viritin.layouts.MHorizontalLayout; 14 | import org.vaadin.viritin.layouts.MVerticalLayout; 15 | 16 | import java.util.function.Consumer; 17 | 18 | public class NoteComponent extends CustomComponent { 19 | 20 | 21 | private Note note; 22 | 23 | public NoteComponent(Note note) { 24 | this.note = note; 25 | setSizeFull(); 26 | setCompositionRoot(initReadOnly()); 27 | } 28 | 29 | public NoteComponent(Note note, Consumer noteConsumer) { 30 | this.note = note; 31 | setSizeFull(); 32 | setCompositionRoot(initEdit(noteConsumer)); 33 | } 34 | 35 | private Component initEdit(Consumer noteConsumer) { 36 | MTextField title = new MTextField("Title", note != null && note.getTitle() != null ? note.getTitle() : ""); 37 | TextArea body = new TextArea("Body"); 38 | body.setValue(note != null && note.getBody() != null ? note.getBody() : ""); 39 | body.setSizeFull(); 40 | 41 | return new MVerticalLayout() 42 | .add(title) 43 | .add(body, 1) 44 | .add(new MButton(FontAwesome.SAVE, "save", e -> { 45 | if (title.getValue() != null && title.getValue() 46 | .trim() 47 | .length() > 0 && body.getValue() != null && body.getValue() 48 | .trim() 49 | .length() > 0) { 50 | noteConsumer.accept(Note.builder() 51 | .title(title.getValue()) 52 | .body(body.getValue()) 53 | .build()); 54 | } else { 55 | Notification.show("please fill title and body!"); 56 | } 57 | })) 58 | .withSize(MSize.FULL_SIZE); 59 | } 60 | 61 | private Component initReadOnly() { 62 | return new MVerticalLayout() 63 | .add(new MLabel(note.getTitle()).withStyleName(ValoTheme.LABEL_BOLD)) 64 | .add(new RichText() 65 | .withMarkDown(note.getBody() != null ? note.getBody() : "") 66 | .withSize(MSize.FULL_SIZE), 1) 67 | .add(new MHorizontalLayout() 68 | .add(new MLabel(note.getCreated() 69 | .toString(DateTimeFormat.longDateTime())) 70 | .withStyleName(ValoTheme.LABEL_SMALL), Alignment.MIDDLE_RIGHT) 71 | .add(new MLabel(note.getUsername()) 72 | .withStyleName(ValoTheme.LABEL_SMALL, ValoTheme.LABEL_COLORED), Alignment.MIDDLE_RIGHT) 73 | .withFullWidth()) 74 | .withSize(MSize.FULL_SIZE); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/form/UserDetailForm.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.form; 2 | 3 | import com.vaadin.server.ExternalResource; 4 | import com.vaadin.shared.ui.ContentMode; 5 | import com.vaadin.ui.*; 6 | import io.rocketbase.toggl.backend.model.ApplicationSetting.UserDetails; 7 | import io.rocketbase.toggl.backend.util.ColorPalette; 8 | import org.vaadin.viritin.fields.LabelField; 9 | import org.vaadin.viritin.form.AbstractForm; 10 | import org.vaadin.viritin.label.MLabel; 11 | import org.vaadin.viritin.layouts.MHorizontalLayout; 12 | import org.vaadin.viritin.layouts.MVerticalLayout; 13 | 14 | import java.util.Arrays; 15 | 16 | 17 | /** 18 | * Created by marten on 09.03.17. 19 | */ 20 | public class UserDetailForm extends AbstractForm { 21 | 22 | private LabelField name = new LabelField("name"); 23 | 24 | private LabelField email = new LabelField("email"); 25 | 26 | private MLabel colorBox = new MLabel("") 27 | .withContentMode(ContentMode.HTML) 28 | .withWidth("37px") 29 | .withHeight("37px"); 30 | 31 | private ComboBox graphColor = new ComboBox<>("colors", Arrays.asList(ColorPalette.values())); 32 | 33 | 34 | private Image avatar; 35 | 36 | public UserDetailForm() { 37 | super(UserDetails.class); 38 | avatar = new Image(null, null); 39 | avatar.setWidth("64px"); 40 | avatar.setHeight("64px"); 41 | 42 | graphColor.setStyleGenerator((StyleGenerator) colorPalette -> "color-palette " + colorPalette.getStyleName()); 43 | graphColor.setItemCaptionGenerator(e -> e.name() 44 | .toLowerCase() 45 | .replace("_", " ")); 46 | graphColor.setEmptySelectionAllowed(false); 47 | graphColor.addValueChangeListener(e -> colorBox.setValue(String.format("
", 48 | e.getValue() 49 | .getHexCode()))); 50 | 51 | getBinder().bind(email, "email"); 52 | getBinder().bind(name, "name"); 53 | getBinder().bind(graphColor, "graphColor"); 54 | } 55 | 56 | @Override 57 | public void setEntity(UserDetails entity) { 58 | super.setEntity(entity); 59 | if (entity != null) { 60 | avatar.setSource(new ExternalResource(entity.getAvatar())); 61 | } else { 62 | avatar.setSource(null); 63 | } 64 | } 65 | 66 | @Override 67 | protected Component createContent() { 68 | return new MVerticalLayout() 69 | .add(new MHorizontalLayout() 70 | .add(new MVerticalLayout() 71 | .add(name) 72 | .add(email) 73 | .add(new MHorizontalLayout() 74 | .add(colorBox, Alignment.MIDDLE_CENTER) 75 | .add(graphColor, Alignment.MIDDLE_CENTER, 1) 76 | .withFullWidth()) 77 | .withMargin(false) 78 | .withFullWidth(), 2) 79 | .add(avatar, Alignment.MIDDLE_CENTER, 1) 80 | .withFullWidth()) 81 | .withMargin(false) 82 | .add(getToolbar()) 83 | .withFullWidth(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/service/FetchAndStoreService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.service; 2 | 3 | import io.rocketbase.toggl.api.model.TimeEntry; 4 | import io.rocketbase.toggl.api.util.FetchAllDetailed; 5 | import io.rocketbase.toggl.backend.config.TogglService; 6 | import io.rocketbase.toggl.backend.model.DateTimeEntryGroup; 7 | import io.rocketbase.toggl.backend.repository.DateTimeEntryGroupRepository; 8 | import lombok.SneakyThrows; 9 | import org.joda.time.DateTime; 10 | import org.joda.time.Days; 11 | import org.joda.time.LocalDate; 12 | import org.joda.time.LocalTime; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.stream.Collectors; 20 | 21 | /** 22 | * Created by marten on 08.03.17. 23 | */ 24 | @Service 25 | public class FetchAndStoreService { 26 | 27 | @Resource 28 | private DateTimeEntryGroupRepository dateTimeEntryGroupRepository; 29 | 30 | @Resource 31 | private TogglService togglService; 32 | 33 | @SneakyThrows 34 | public void fetchBetween(LocalDate from, LocalDate to) { 35 | 36 | 37 | DateTime fetchedDate = DateTime.now(); 38 | List timeEntryList = new ArrayList<>(); 39 | 40 | if (Days.daysBetween(from, to) 41 | .getDays() > 100) { 42 | LocalDate startFrom = from; 43 | do { 44 | LocalDate toSub = startFrom.plusDays(99); 45 | if (to.isBefore(toSub)) { 46 | toSub = to; 47 | } 48 | timeEntryList.addAll(fetch(startFrom, toSub)); 49 | startFrom = startFrom.plusDays(100); 50 | } while (startFrom.isBefore(to)); 51 | } else { 52 | timeEntryList.addAll(fetch(from, to)); 53 | } 54 | 55 | dateTimeEntryGroupRepository.deleteByWorkspaceIdAndDateBetween(togglService.getWorkspaceId(), 56 | from 57 | .minusDays(1) 58 | .toDate(), 59 | to 60 | .plusDays(1) 61 | .toDate()); 62 | 63 | 64 | List newEntities = new ArrayList<>(); 65 | 66 | Map> dateListMap = timeEntryList.stream() 67 | .collect(Collectors.groupingBy(e -> e.getStart() 68 | .toLocalDate())); 69 | 70 | dateListMap.forEach((date, timeEntries) -> { 71 | newEntities.add(DateTimeEntryGroup.builder() 72 | .date(date) 73 | .workspaceId(togglService.getWorkspaceId()) 74 | .fetched(fetchedDate) 75 | .userTimeEntriesMap(timeEntries.stream() 76 | .collect(Collectors.groupingBy(TimeEntry::getUserId))) 77 | .build()); 78 | }); 79 | dateTimeEntryGroupRepository.save(newEntities); 80 | 81 | } 82 | 83 | private List fetch(LocalDate from, LocalDate to) { 84 | return FetchAllDetailed.getAll(togglService.getTogglReportApi() 85 | .detailed() 86 | .since(from.toDateTime(LocalTime.MIDNIGHT) 87 | .toDate()) 88 | .until(to.toDateTime(new LocalTime(23, 59, 59, 999)) 89 | .toDate())); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/form/MongoUserForm.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.form; 2 | 3 | import com.vaadin.ui.*; 4 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 5 | import io.rocketbase.toggl.backend.security.UserRole; 6 | import org.vaadin.viritin.form.AbstractForm; 7 | import org.vaadin.viritin.layouts.MVerticalLayout; 8 | 9 | import java.util.Arrays; 10 | 11 | public class MongoUserForm extends AbstractForm { 12 | 13 | private TextField username = new TextField("username"); 14 | 15 | private PasswordField newPassword = new PasswordField("password"); 16 | 17 | private CheckBox enabled = new CheckBox("enabled"); 18 | 19 | private boolean createNew = false; 20 | 21 | private ComboBox role = new ComboBox("role", Arrays.asList(UserRole.values())); 22 | 23 | private Image avatar; 24 | 25 | public MongoUserForm(MongoUserDetails bean) { 26 | super(MongoUserDetails.class); 27 | 28 | username.setWidth("100%"); 29 | newPassword.setWidth("100%"); 30 | 31 | role.setWidth("100%"); 32 | role.setItemCaptionGenerator(e -> e.name() 33 | .toLowerCase() 34 | .replace("_", " ")); 35 | 36 | 37 | setModalWindowTitle("edit login-user"); 38 | setEntity(bean); 39 | newPassword.setVisible(false); 40 | 41 | 42 | getBinder().bind(username, "username"); 43 | getBinder().bind(enabled, "enabled"); 44 | getBinder().bind(role, "role"); 45 | } 46 | 47 | public Window initCreateWindow(SavedHandler handler) { 48 | createNew = true; 49 | setSaveCaption("create"); 50 | newPassword.setVisible(true); 51 | 52 | Window window = openInModalPopup(); 53 | window.setWidth("500px"); 54 | setSavedHandler((SavedHandler) entity -> { 55 | handler.onSave(entity); 56 | window.close(); 57 | }); 58 | return window; 59 | } 60 | 61 | public Window initEditWindow(SavedHandler savedHandler, DeleteHandler deleteHandler) { 62 | Window window = openInModalPopup(); 63 | window.setWidth("500px"); 64 | 65 | setSavedHandler((SavedHandler) entity -> { 66 | savedHandler.onSave(entity); 67 | window.close(); 68 | }); 69 | setDeleteHandler((DeleteHandler) entity -> { 70 | deleteHandler.onDelete(entity); 71 | window.close(); 72 | }); 73 | 74 | return window; 75 | } 76 | 77 | @Override 78 | public MongoUserDetails getEntity() { 79 | MongoUserDetails enitity = super.getEntity(); 80 | if (createNew) { 81 | enitity.setPassword(newPassword.getValue()); 82 | } 83 | return enitity; 84 | } 85 | 86 | @Override 87 | protected Component createContent() { 88 | return new MVerticalLayout() 89 | .add(new MVerticalLayout() 90 | .add(username) 91 | .add(newPassword) 92 | .add(role) 93 | .add(enabled) 94 | .withMargin(false) 95 | .withFullWidth()) 96 | .add(getToolbar()) 97 | .withFullWidth(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 121 | 122 | 123 | 124 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/worker/form/ContractTermsForm.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.worker.form; 2 | 3 | import com.vaadin.data.converter.StringToBigDecimalConverter; 4 | import com.vaadin.ui.CheckBoxGroup; 5 | import com.vaadin.ui.Component; 6 | import com.vaadin.ui.DateField; 7 | import com.vaadin.ui.Window; 8 | import com.vaadin.ui.themes.ValoTheme; 9 | import io.rocketbase.toggl.backend.model.worker.ContractTerms; 10 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 11 | import org.vaadin.viritin.fields.IntegerField; 12 | import org.vaadin.viritin.fields.MTextField; 13 | import org.vaadin.viritin.form.AbstractForm; 14 | import org.vaadin.viritin.layouts.MHorizontalLayout; 15 | import org.vaadin.viritin.layouts.MVerticalLayout; 16 | 17 | import java.time.DayOfWeek; 18 | 19 | public class ContractTermsForm extends AbstractForm { 20 | 21 | private MTextField name = new MTextField("name") 22 | .withFullWidth(); 23 | 24 | private CheckBoxGroup weeklyWorkingDays = new CheckBoxGroup<>("Working Days"); 25 | 26 | private MTextField weeklyWorkingHours = new MTextField("weekly working hours") 27 | .withFullWidth(); 28 | 29 | private IntegerField daysOfVacationPerYear = new IntegerField("days of vacation per year") 30 | .withFullWidth(); 31 | 32 | private MTextField grossMonthlySalary = new MTextField("gross monthly salary") 33 | .withFullWidth(); 34 | 35 | private MTextField netMonthlySalary = new MTextField("net monthly salary") 36 | .withFullWidth(); 37 | 38 | private DateField validFromYoda = new DateField("valid from"); 39 | 40 | private DateField validToYoda = new DateField("valid to"); 41 | 42 | public ContractTermsForm() { 43 | super(ContractTerms.class); 44 | 45 | weeklyWorkingDays.setDescription("used to leave calculations"); 46 | weeklyWorkingDays.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); 47 | weeklyWorkingDays.setItems(DayOfWeek.values()); 48 | weeklyWorkingDays.setWidth("100%"); 49 | 50 | getBinder().bind(name, "name"); 51 | getBinder().bind(weeklyWorkingDays, "weeklyWorkingDays"); 52 | getBinder().bind(daysOfVacationPerYear, "daysOfVacationPerYear"); 53 | 54 | getBinder().forField(weeklyWorkingHours) 55 | .withNullRepresentation("") 56 | .withConverter(new StringToBigDecimalConverter("invalid input")) 57 | .bind("weeklyWorkingHours"); 58 | 59 | getBinder().forField(grossMonthlySalary) 60 | .withNullRepresentation("") 61 | .withConverter(new StringToBigDecimalConverter("invalid input")) 62 | .bind("grossMonthlySalary"); 63 | 64 | getBinder().forField(netMonthlySalary) 65 | .withNullRepresentation("") 66 | .withConverter(new StringToBigDecimalConverter("invalid input")) 67 | .bind("netMonthlySalary"); 68 | } 69 | 70 | @Override 71 | public Window openInModalPopup() { 72 | Window window = super.openInModalPopup(); 73 | window.setWidth("600px"); 74 | return window; 75 | } 76 | 77 | @Override 78 | public ContractTerms getEntity() { 79 | ContractTerms entity = super.getEntity(); 80 | entity.setValidFrom(validFromYoda.getValue() != null ? LocalDateConverter.convert(validFromYoda.getValue()) : null); 81 | entity.setValidTo(validToYoda.getValue() != null ? LocalDateConverter.convert(validToYoda.getValue()) : null); 82 | return entity; 83 | } 84 | 85 | @Override 86 | protected Component createContent() { 87 | validFromYoda.setWidth("100%"); 88 | validToYoda.setWidth("100%"); 89 | 90 | return new MVerticalLayout() 91 | .add(name) 92 | .add(weeklyWorkingDays) 93 | .add(new MHorizontalLayout() 94 | .add(weeklyWorkingHours, 1) 95 | .add(daysOfVacationPerYear, 1) 96 | .withMargin(false) 97 | .withFullWidth()) 98 | .add(new MHorizontalLayout() 99 | .add(grossMonthlySalary, 1) 100 | .add(netMonthlySalary, 1) 101 | .withMargin(false) 102 | .withFullWidth()) 103 | .add(new MHorizontalLayout() 104 | .add(validFromYoda, 1) 105 | .add(validToYoda, 1) 106 | .withMargin(false) 107 | .withFullWidth()) 108 | .add(getToolbar()) 109 | .withFullWidth(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/tab/SettingTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.tab; 2 | 3 | import com.vaadin.icons.VaadinIcons; 4 | import com.vaadin.spring.annotation.SpringComponent; 5 | import com.vaadin.spring.annotation.UIScope; 6 | import com.vaadin.ui.Alignment; 7 | import com.vaadin.ui.CheckBoxGroup; 8 | import com.vaadin.ui.ComboBox; 9 | import com.vaadin.ui.Component; 10 | import com.vaadin.ui.themes.ValoTheme; 11 | import de.jollyday.HolidayCalendar; 12 | import io.rocketbase.toggl.backend.config.TogglService; 13 | import io.rocketbase.toggl.backend.model.ApplicationSetting.UserDetails; 14 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 15 | import io.rocketbase.toggl.ui.view.setting.form.UserDetailForm; 16 | import org.vaadin.viritin.MSize; 17 | import org.vaadin.viritin.button.MButton; 18 | import org.vaadin.viritin.label.MLabel; 19 | import org.vaadin.viritin.layouts.MHorizontalLayout; 20 | import org.vaadin.viritin.layouts.MVerticalLayout; 21 | 22 | import javax.annotation.Resource; 23 | import java.time.DayOfWeek; 24 | import java.util.Arrays; 25 | import java.util.HashSet; 26 | 27 | /** 28 | * Created by marten on 09.03.17. 29 | */ 30 | @UIScope 31 | @SpringComponent 32 | public class SettingTab extends AbstractTab { 33 | 34 | @Resource 35 | private TogglService togglService; 36 | 37 | private ComboBox holidayCalendarTypedSelect; 38 | 39 | private CheckBoxGroup workingDaysSelect; 40 | 41 | private ComboBox userTypedSelect; 42 | 43 | private UserDetailForm userDetailForm; 44 | 45 | private MLabel placeHolder = new MLabel(""); 46 | 47 | @Override 48 | public Component initLayout() { 49 | userDetailForm = new UserDetailForm(); 50 | userDetailForm.setSavedHandler((e) -> { 51 | togglService.updateUser(e); 52 | refreshTab(); 53 | }); 54 | userDetailForm.setVisible(false); 55 | 56 | holidayCalendarTypedSelect = initHolidayCalenderTypeSelect(); 57 | workingDaysSelect = initWorkingDaysSelect(); 58 | userTypedSelect = initUserTypeSelect(); 59 | 60 | MButton refreshUsers = new MButton(VaadinIcons.REFRESH, "Refresh Users", event -> { 61 | togglService.updateCurrentWorkspaceUsers(); 62 | onTabEnter(); 63 | }); 64 | return new MVerticalLayout() 65 | .add(holidayCalendarTypedSelect) 66 | .add(workingDaysSelect) 67 | .add(new MHorizontalLayout().add(userTypedSelect, 1) 68 | .add(refreshUsers, Alignment.BOTTOM_CENTER) 69 | .withFullWidth()) 70 | .add(userDetailForm, 1) 71 | .add(placeHolder, 1) 72 | .withSize(MSize.FULL_SIZE); 73 | } 74 | 75 | 76 | private ComboBox initHolidayCalenderTypeSelect() { 77 | ComboBox combo = new ComboBox<>("HolidayCalendar", Arrays.asList(HolidayCalendar.values())); 78 | combo.setDescription("get displayed in below detailed statistics"); 79 | combo.setWidth("100%"); 80 | combo.addValueChangeListener(e -> togglService.updateHolidayCalendar(e.getValue())); 81 | return combo; 82 | } 83 | 84 | private CheckBoxGroup initWorkingDaysSelect() { 85 | CheckBoxGroup checkBoxGroup = new CheckBoxGroup<>("Working Days"); 86 | checkBoxGroup.setDescription("used to leave calculations"); 87 | checkBoxGroup.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); 88 | checkBoxGroup.setItems(DayOfWeek.values()); 89 | checkBoxGroup.addValueChangeListener(e -> { 90 | togglService.updateRegularWorkinsDays(e.getValue()); 91 | }); 92 | checkBoxGroup.setWidth("100%"); 93 | return checkBoxGroup; 94 | } 95 | 96 | private ComboBox initUserTypeSelect() { 97 | ComboBox combo = new ComboBox<>("User-Settings"); 98 | combo.setItemCaptionGenerator(u -> u.getName()); 99 | combo.setWidth("100%"); 100 | combo.addValueChangeListener(e -> { 101 | userDetailForm.setVisible(e.getValue() != null); 102 | placeHolder.setVisible(e.getValue() == null); 103 | if (e.getValue() != null) { 104 | userDetailForm.setEntity(e.getValue()); 105 | } 106 | }); 107 | return combo; 108 | } 109 | 110 | @Override 111 | public void onTabEnter() { 112 | userTypedSelect.setItems(togglService.getAllUsers()); 113 | holidayCalendarTypedSelect.setValue(togglService.getHolidayCalender()); 114 | workingDaysSelect.setValue(new HashSet<>(togglService.getRegularWorkinsDays())); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/worker/WorkerView.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.worker; 2 | 3 | import com.vaadin.data.ValueProvider; 4 | import com.vaadin.icons.VaadinIcons; 5 | import com.vaadin.navigator.ViewChangeListener; 6 | import com.vaadin.spring.annotation.SpringView; 7 | import com.vaadin.spring.annotation.UIScope; 8 | import com.vaadin.ui.Alignment; 9 | import com.vaadin.ui.Component; 10 | import com.vaadin.ui.Grid; 11 | import com.vaadin.ui.Window; 12 | import com.vaadin.ui.themes.ValoTheme; 13 | import io.rocketbase.toggl.backend.model.Worker; 14 | import io.rocketbase.toggl.backend.model.worker.ContractTerms; 15 | import io.rocketbase.toggl.backend.security.UserRole; 16 | import io.rocketbase.toggl.backend.service.WorkerService; 17 | import io.rocketbase.toggl.ui.view.AbstractView; 18 | import io.rocketbase.toggl.ui.view.worker.form.ContractTermsForm; 19 | import io.rocketbase.toggl.ui.view.worker.form.WorkerForm; 20 | import org.springframework.context.ApplicationContext; 21 | import org.vaadin.viritin.MSize; 22 | import org.vaadin.viritin.button.MButton; 23 | import org.vaadin.viritin.form.AbstractForm; 24 | import org.vaadin.viritin.layouts.MVerticalLayout; 25 | 26 | import javax.annotation.Resource; 27 | import java.util.ArrayList; 28 | 29 | @UIScope 30 | @SpringView(name = WorkerView.VIEW_NAME) 31 | public class WorkerView extends AbstractView { 32 | 33 | public static final String VIEW_NAME = "worker"; 34 | 35 | @Resource 36 | private WorkerService workerService; 37 | 38 | @Resource 39 | private ApplicationContext applicationContext; 40 | 41 | private Grid workerGrid; 42 | 43 | private Window contractTermsFormWindow = null; 44 | 45 | 46 | public WorkerView() { 47 | super(VIEW_NAME, "Worker", VaadinIcons.USERS, 10); 48 | setDevelopmentMode(true); 49 | setUserRole(UserRole.ROLE_ADMIN); 50 | } 51 | 52 | @Override 53 | public Component initialzeUi() { 54 | workerGrid = initWorkerGrid(); 55 | return new MVerticalLayout() 56 | .add(new MButton(VaadinIcons.PLUS, "add worker", e -> { 57 | openEditForm(new Worker()); 58 | }), Alignment.TOP_RIGHT) 59 | .add(workerGrid, 1) 60 | .withSize(MSize.FULL_SIZE); 61 | } 62 | 63 | private void openEditForm(Worker worker) { 64 | WorkerForm form = applicationContext.getBean(WorkerForm.class); 65 | form.setEntity(worker); 66 | form.setSavedHandler(bean -> { 67 | workerService.updateWorker(bean); 68 | form.closePopup(); 69 | reloadGrid(); 70 | }); 71 | form.openInModalPopup(); 72 | } 73 | 74 | private Grid initWorkerGrid() { 75 | Grid grid = new Grid<>(Worker.class); 76 | grid.setColumns(); 77 | grid.addColumn("fullName") 78 | .setCaption("Name"); 79 | grid.addColumn("dateOfJoining") 80 | .setCaption("Date joined"); 81 | grid.addComponentColumn((ValueProvider) bean -> new MButton(VaadinIcons.PENCIL, e -> openEditForm(bean)) 82 | .withStyleName(ValoTheme.BUTTON_BORDERLESS)) 83 | .setCaption("edit") 84 | .setWidth(100); 85 | grid.addComponentColumn((ValueProvider) bean -> new MButton(VaadinIcons.WRENCH, e -> { 86 | ContractTermsForm form = new ContractTermsForm(); 87 | form.setEntity(bean.getContractTerms() != null && !bean.getContractTerms() 88 | .isEmpty() ? bean.getContractTerms() 89 | .get(0) : new ContractTerms()); 90 | 91 | form.setSavedHandler((AbstractForm.SavedHandler) contractTerms -> { 92 | if (bean.getContractTerms() == null) { 93 | bean.setContractTerms(new ArrayList<>()); 94 | bean.getContractTerms() 95 | .add(contractTerms); 96 | workerService.updateWorker(bean); 97 | contractTermsFormWindow.close(); 98 | reloadGrid(); 99 | } 100 | }); 101 | contractTermsFormWindow = form.openInModalPopup(); 102 | }) 103 | .withStyleName(ValoTheme.BUTTON_BORDERLESS)) 104 | .setCaption("contact") 105 | .setWidth(100); 106 | grid.setSizeFull(); 107 | return grid; 108 | } 109 | 110 | @Override 111 | public void enter(ViewChangeListener.ViewChangeEvent viewChangeEvent) { 112 | super.enter(viewChangeEvent); 113 | reloadGrid(); 114 | } 115 | 116 | private void reloadGrid() { 117 | workerGrid.setItems(workerService.findAll()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/tab/LoginUserTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.tab; 2 | 3 | import com.vaadin.data.provider.DataProvider; 4 | import com.vaadin.icons.VaadinIcons; 5 | import com.vaadin.spring.annotation.SpringComponent; 6 | import com.vaadin.spring.annotation.UIScope; 7 | import com.vaadin.ui.*; 8 | import com.vaadin.ui.Notification.Type; 9 | import com.vaadin.ui.themes.ValoTheme; 10 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 11 | import io.rocketbase.toggl.backend.security.MongoUserService; 12 | import io.rocketbase.toggl.backend.security.UserRole; 13 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 14 | import io.rocketbase.toggl.ui.view.setting.form.MongoUserForm; 15 | import io.rocketbase.toggl.ui.view.setting.window.LinkWorkerWindow; 16 | import org.springframework.context.ApplicationContext; 17 | import org.vaadin.viritin.MSize; 18 | import org.vaadin.viritin.button.MButton; 19 | import org.vaadin.viritin.button.PrimaryButton; 20 | import org.vaadin.viritin.form.AbstractForm; 21 | import org.vaadin.viritin.layouts.MVerticalLayout; 22 | import org.vaadin.viritin.layouts.MWindow; 23 | 24 | import javax.annotation.Resource; 25 | 26 | /** 27 | * Created by marten on 09.03.17. 28 | */ 29 | @UIScope 30 | @SpringComponent 31 | public class LoginUserTab extends AbstractTab { 32 | 33 | @Resource 34 | private MongoUserService mongoUserService; 35 | 36 | @Resource 37 | private ApplicationContext applicationContext; 38 | 39 | private Grid userTable; 40 | 41 | @Override 42 | public Component initLayout() { 43 | userTable = initUserTable(); 44 | 45 | return new MVerticalLayout() 46 | .add(new MButton(VaadinIcons.PLUS, "add", e -> { 47 | new MongoUserForm(MongoUserDetails.builder() 48 | .role(UserRole.ROLE_USER) 49 | .build()) 50 | .initCreateWindow((AbstractForm.SavedHandler) entity -> { 51 | mongoUserService.register(entity); 52 | }) 53 | .addCloseListener(closeEvent -> { 54 | refreshTab(); 55 | }); 56 | }), Alignment.MIDDLE_RIGHT) 57 | .add(userTable, 1) 58 | .withSize(MSize.FULL_SIZE); 59 | } 60 | 61 | private Grid initUserTable() { 62 | Grid table = new Grid<>(); 63 | table.addColumn(MongoUserDetails::getUsername) 64 | .setExpandRatio(1); 65 | table.addColumn(MongoUserDetails::getRole); 66 | table.addColumn(MongoUserDetails::isEnabled); 67 | table.addComponentColumn(user -> new MButton(VaadinIcons.USER, 68 | user.getWorker() != null ? user.getWorker() 69 | .getFullName() : "not linked", 70 | e -> { 71 | LinkWorkerWindow window = applicationContext.getBean(LinkWorkerWindow.class); 72 | window.linkUser(user); 73 | window.addCloseListener(closeEvent -> onTabEnter()); 74 | UI.getCurrent() 75 | .addWindow(window); 76 | }).withStyleName(ValoTheme.BUTTON_BORDERLESS)) 77 | .setCaption("linked worker") 78 | .setWidth(200); 79 | 80 | table.addComponentColumn(user -> new MButton(VaadinIcons.KEY, e -> { 81 | PasswordField password = new PasswordField("password"); 82 | password.setRequiredIndicatorVisible(true); 83 | password.setWidth("100%"); 84 | 85 | MWindow window = new MWindow("change password") 86 | .withModal(true) 87 | .withDraggable(false) 88 | .withResizable(false) 89 | .withCenter(); 90 | window.setContent(new MVerticalLayout().add(password) 91 | .add(new PrimaryButton("change", changeEvent -> { 92 | mongoUserService.updatePassword(user, password.getValue()); 93 | Notification.show("successfully changed password"); 94 | window.close(); 95 | })) 96 | .withWidth("300px")); 97 | UI.getCurrent() 98 | .addWindow(window); 99 | }).withStyleName(ValoTheme.BUTTON_BORDERLESS, ValoTheme.BUTTON_ICON_ONLY)) 100 | .setCaption("password") 101 | .setWidth(80); 102 | 103 | table.addComponentColumn(user -> new MButton(VaadinIcons.PENCIL, e -> { 104 | new MongoUserForm(user) 105 | .initEditWindow((AbstractForm.SavedHandler) entity -> { 106 | mongoUserService.updateDetailsExceptPassword(entity); 107 | }, (AbstractForm.DeleteHandler) entity -> { 108 | try { 109 | mongoUserService.delete(entity); 110 | } catch (Exception exception) { 111 | Notification.show("you cannot delete yourself!", Type.ERROR_MESSAGE); 112 | } 113 | }) 114 | .addCloseListener(closeEvent -> { 115 | refreshTab(); 116 | }); 117 | }).withStyleName(ValoTheme.BUTTON_BORDERLESS, ValoTheme.BUTTON_ICON_ONLY)) 118 | .setCaption("edit") 119 | .setWidth(80); 120 | 121 | table.setSizeFull(); 122 | return table; 123 | } 124 | 125 | 126 | @Override 127 | public void onTabEnter() { 128 | userTable.setDataProvider(DataProvider.ofCollection(mongoUserService.findAll())); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/util/ColorPalette.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.util; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.util.*; 7 | 8 | /** 9 | * holds a list of colors
10 | * provides a function to transform from hex-color to rgb-color
11 | * furthermore it allows to find a nearest fitting color to given hex-code 12 | */ 13 | public enum ColorPalette { 14 | 15 | BRICK("a54e3c"), 16 | POMEGRANTE("881f30"), 17 | CABERNET("721432"), 18 | AUBERGINE("5b1946"), 19 | INDIGO("461c5d"), 20 | PLUM("5c145e"), 21 | ROYAL("97348a"), 22 | FRENCH_BLUE("3266ad"), 23 | TEAL("337b8d"), 24 | AQUAMARINE("22586f"), 25 | MARINE("18456b"), 26 | NAVY("0e2e57"), 27 | LILAC("ae9cc7"), 28 | ORCHID("985a9c"), 29 | COBALT("4099d4"), 30 | POOL("9cd6f5"), 31 | PERIWINKLE("a7b4d0"), 32 | DOVE("aec4d0"), 33 | DUCK_EGG("97babf"), 34 | TURQUOISE("6fbabf"), 35 | JADE("73aba0"), 36 | TIFFANY("a5d2c1"), 37 | MINT("c8e0c4"), 38 | SPRING("d3e2a3"), 39 | CHIVE("b8d26e"), 40 | TREE_TOP("759150"), 41 | BOTTLE("378159"), 42 | EMERALD("29634e"), 43 | LEMON("fef6a6"), 44 | SUNSHINE("fcf151"), 45 | TUMERIC("f9d549"), 46 | CLEMENTINE("f3b143"), 47 | PEACH("f7d19d"), 48 | SALMON("f0b79f"), 49 | CORAL("e89487"), 50 | WATERMELON("e3708e"), 51 | RASBERRY("ac2c69"), 52 | MAGENTA("db318a"), 53 | RED_BALLOON("db3656"), 54 | FIRE_ENGINE("db3732"), 55 | BLOOD_ORANGE("e06151"), 56 | PAPAYA("e88b5b"), 57 | ROSE("e6aab3"), 58 | CHERRY_BLOSSOM("f6d6dc"), 59 | BLUSH("f6d6d1"), 60 | FRENCH_VANILLA("f8f1de"), 61 | IVORY("fbf8da"), 62 | CLAY("e8d4b8"), 63 | SANDSTONE("c4b199"), 64 | SAGE("a5b4a3"), 65 | MOSS("a2a495"), 66 | OLIVE("7b7d6a"), 67 | SMOKE("63605c"), 68 | ESPRESSO("473937"), 69 | CHOCOLATE("684940"), 70 | CARAMEL("957d62"), 71 | BLACK("221f20"), 72 | CHARCOAL("58585a"), 73 | SLATE("949599"), 74 | SILVER_LINING("d1d2d4"), 75 | OYSTER("e3ddd4"), 76 | STONE("d2cfc9"), 77 | SMOG("928b88"), 78 | WHITE("ffffff"); 79 | 80 | public static final String STYLE_BASE_NAME = "color-palette"; 81 | @Getter 82 | private String hexCode; 83 | 84 | ColorPalette(String hexCode) { 85 | this.hexCode = hexCode; 86 | } 87 | 88 | /** 89 | * find a nearest fitting @{@link ColorPalette} by given hexcode 90 | * 91 | * @param hexCode sting of color could start with # or not... 92 | * @return when no problem @{@link ColorPalette} 93 | */ 94 | public static ColorPalette getNearest(String hexCode) { 95 | RgbColor given = hex2rgb(hexCode); 96 | if (given == null) { 97 | return null; 98 | } 99 | List differenceArray = new ArrayList<>(); 100 | Arrays.asList(values()) 101 | .forEach(color -> { 102 | RgbColor c = color.getRgbColor(); 103 | differenceArray.add(Math.sqrt((given.getR() - c.getR()) * (given.getR() - c.getR()) 104 | + (given.getG() - c.getG()) * (given.getG() - c.getG()) 105 | + (given.getB() - c.getB()) * (given.getB() - c.getB()))); 106 | }); 107 | Double min = Collections.min(differenceArray); 108 | int index = differenceArray.indexOf(min); 109 | 110 | return values()[index]; 111 | } 112 | 113 | public static ColorPalette getRandomValue() { 114 | return values()[new Random().nextInt(values().length)]; 115 | } 116 | 117 | /** 118 | * read's given hex string and returns a it's color representation by red, green, blue 119 | * 120 | * @param hexCode sting of color could start with # or not... 121 | * @return @{@link RgbColor} 122 | */ 123 | public static RgbColor hex2rgb(String hexCode) { 124 | if (hexCode == null) { 125 | return null; 126 | } 127 | String colour = new String(hexCode); 128 | if (colour.startsWith("#")) { 129 | colour = colour.substring(1); 130 | } 131 | 132 | int r = 0, g = 0, b = 0; 133 | 134 | if (colour.length() == 6) { 135 | r = Integer.valueOf(colour.substring(0, 1), 16); 136 | g = Integer.valueOf(colour.substring(2, 3), 16); 137 | b = Integer.valueOf(colour.substring(4, 5), 16); 138 | } else if (colour.length() == 3) { 139 | r = Integer.valueOf(colour.substring(0, 1) + colour.substring(0, 1), 16); 140 | g = Integer.valueOf(colour.substring(1, 2) + colour.substring(1, 2), 16); 141 | b = Integer.valueOf(colour.substring(2, 3) + colour.substring(2, 3), 16); 142 | } else { 143 | return null; 144 | } 145 | 146 | return new RgbColor(r, g, b); 147 | } 148 | 149 | public RgbColor getRgbColor() { 150 | return hex2rgb(this.getHexCode()); 151 | } 152 | 153 | public static ColorPalette getRandomValue(Collection existingCollection) { 154 | ColorPalette newValue = null; 155 | if (existingCollection == null || existingCollection.size() >= values().length) { 156 | return null; 157 | } 158 | do { 159 | newValue = getRandomValue(); 160 | } while (!existingCollection.contains(newValue)); 161 | return newValue; 162 | } 163 | 164 | public String getStyleName() { 165 | return STYLE_BASE_NAME + "-" + 166 | this.name() 167 | .toLowerCase() 168 | .replace("_", "-"); 169 | } 170 | 171 | /** 172 | * calculate the best constrast color to given color 173 | * 174 | * @return when true text-color should be black otherwise white 175 | */ 176 | public boolean isTextConstrastBlack() { 177 | RgbColor color = getRgbColor(); 178 | // return (color.getR() * 0.299 + color.getG() * 0.587 + color.getB() * 0.114) > 186 ? true : false; 179 | return (Integer.valueOf(hexCode, 16) > 0xffffff / 2) ? true : false; 180 | } 181 | 182 | @RequiredArgsConstructor 183 | @Getter 184 | public static class RgbColor { 185 | /** 186 | * red 187 | */ 188 | private final int r; 189 | /** 190 | * green 191 | */ 192 | private final int g; 193 | /** 194 | * blue 195 | */ 196 | private final int b; 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/home/tab/ChartTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.home.tab; 2 | 3 | import com.byteowls.vaadin.chartjs.ChartJs; 4 | import com.byteowls.vaadin.chartjs.config.LineChartConfig; 5 | import com.byteowls.vaadin.chartjs.data.LineDataset; 6 | import com.byteowls.vaadin.chartjs.options.InteractionMode; 7 | import com.byteowls.vaadin.chartjs.options.Position; 8 | import com.byteowls.vaadin.chartjs.options.Tooltips; 9 | import com.byteowls.vaadin.chartjs.options.scale.Axis; 10 | import com.byteowls.vaadin.chartjs.options.scale.CategoryScale; 11 | import com.byteowls.vaadin.chartjs.options.scale.LinearScale; 12 | import com.vaadin.spring.annotation.SpringComponent; 13 | import com.vaadin.spring.annotation.UIScope; 14 | import com.vaadin.ui.Alignment; 15 | import com.vaadin.ui.ComboBox; 16 | import com.vaadin.ui.Component; 17 | import io.rocketbase.toggl.backend.model.report.UserTimeline; 18 | import io.rocketbase.toggl.backend.service.TimeEntryService; 19 | import io.rocketbase.toggl.backend.util.YearMonthUtil; 20 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 21 | import io.rocketbase.toggl.ui.view.home.HomeView; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.joda.time.DateTimeConstants; 24 | import org.joda.time.YearMonth; 25 | import org.vaadin.viritin.MSize; 26 | import org.vaadin.viritin.layouts.MHorizontalLayout; 27 | import org.vaadin.viritin.layouts.MPanel; 28 | import org.vaadin.viritin.layouts.MVerticalLayout; 29 | 30 | import javax.annotation.Resource; 31 | import java.util.List; 32 | import java.util.stream.Collectors; 33 | 34 | 35 | @UIScope 36 | @SpringComponent 37 | @Slf4j 38 | public class ChartTab extends AbstractTab { 39 | 40 | @Resource 41 | private TimeEntryService timeEntryService; 42 | 43 | private MPanel panel; 44 | 45 | private ComboBox typedSelect = new ComboBox<>(); 46 | 47 | @Override 48 | public Component initLayout() { 49 | panel = new MPanel(HomeView.getPlaceHolder()) 50 | .withSize(MSize.FULL_SIZE); 51 | 52 | typedSelect.setEmptySelectionAllowed(false); 53 | typedSelect.setTextInputAllowed(false); 54 | typedSelect.setWidth("100%"); 55 | typedSelect.addValueChangeListener(e -> filter()); 56 | 57 | return new MVerticalLayout() 58 | .add(new MHorizontalLayout() 59 | .add(typedSelect, Alignment.MIDDLE_RIGHT) 60 | .withFullWidth()) 61 | .add(panel, 1) 62 | .withSize(MSize.FULL_SIZE); 63 | } 64 | 65 | @Override 66 | public void onTabEnter() { 67 | List items = timeEntryService.fetchAllYearMonths(); 68 | typedSelect.setItems(items); 69 | if (typedSelect.getValue() == null && items != null && !items.isEmpty()) { 70 | typedSelect.setValue(items.get(0)); 71 | } 72 | filter(); 73 | } 74 | 75 | private void filter() { 76 | if (typedSelect.getValue() != null) { 77 | 78 | MVerticalLayout layout = new MVerticalLayout() 79 | .add(genChart(typedSelect.getValue()), 1) 80 | .withMargin(false) 81 | .withStyleName("chart-container") 82 | .withSize(MSize.FULL_SIZE); 83 | 84 | panel.setContent(new MVerticalLayout() 85 | .add(layout, Alignment.MIDDLE_CENTER, 1) 86 | .withSize(MSize.FULL_SIZE)); 87 | 88 | } else { 89 | panel.setContent(HomeView.getPlaceHolder()); 90 | } 91 | } 92 | 93 | protected ChartJs genChart(YearMonth yearMonth) { 94 | LineChartConfig config = new LineChartConfig(); 95 | 96 | config.data() 97 | .labelsAsList(YearMonthUtil.getAllDatesOfMonth(yearMonth) 98 | .stream() 99 | .map(d -> { 100 | if (d.getDayOfWeek() > DateTimeConstants.FRIDAY) { 101 | return String.format("(%d)", d.getDayOfMonth()); 102 | } else { 103 | return String.valueOf(d.getDayOfMonth()); 104 | } 105 | }) 106 | .collect(Collectors.toList())); 107 | 108 | List userTimelineList = timeEntryService.getUserTimeLines(yearMonth); 109 | 110 | userTimelineList.forEach(userTimeline -> { 111 | String color = "#" + userTimeline.getUser() 112 | .getGraphColor() 113 | .getHexCode(); 114 | config.data() 115 | .addDataset(new LineDataset().label(userTimeline.getUser() 116 | .getName()) 117 | .backgroundColor(color) 118 | .borderColor(color) 119 | .dataAsList(userTimeline.getHoursWorked()) 120 | .fill(false)); 121 | }); 122 | 123 | config.options() 124 | .responsive(true) 125 | .title() 126 | .display(true) 127 | .text(yearMonth.toString()) 128 | .and() 129 | .tooltips() 130 | .position(Tooltips.PositionMode.AVERAGE) 131 | .mode(InteractionMode.INDEX) 132 | .intersect(false) 133 | .and() 134 | .scales() 135 | .add(Axis.X, new CategoryScale() 136 | .display(true) 137 | .scaleLabel() 138 | .display(true) 139 | .labelString("day of month") 140 | .and() 141 | .position(Position.BOTTOM)) 142 | .add(Axis.Y, new LinearScale() 143 | .display(true) 144 | .scaleLabel() 145 | .display(true) 146 | .labelString("Hours") 147 | .and() 148 | .ticks() 149 | .min(0) 150 | .max(14) 151 | .and() 152 | .position(Position.RIGHT)) 153 | .and() 154 | .maintainAspectRatio(true) 155 | .done(); 156 | 157 | ChartJs chart = new ChartJs(config); 158 | chart.setWidth(100, Unit.PERCENTAGE); 159 | chart.addStyleName("home-container"); 160 | 161 | return chart; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/model/report/UserTimeline.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.model.report; 2 | 3 | import io.rocketbase.toggl.api.model.TimeEntry; 4 | import io.rocketbase.toggl.backend.model.ApplicationSetting.UserDetails; 5 | import io.rocketbase.toggl.backend.util.YearMonthUtil; 6 | import io.rocketbase.toggl.backend.util.YearWeekUtil; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import org.joda.time.LocalDate; 10 | import org.joda.time.Period; 11 | import org.joda.time.YearMonth; 12 | import org.joda.time.format.PeriodFormatter; 13 | import org.joda.time.format.PeriodFormatterBuilder; 14 | import org.threeten.extra.YearWeek; 15 | 16 | import java.util.*; 17 | import java.util.Map.Entry; 18 | import java.util.concurrent.atomic.AtomicLong; 19 | import java.util.stream.Collectors; 20 | 21 | @RequiredArgsConstructor 22 | public class UserTimeline { 23 | 24 | 25 | public static final PeriodFormatter PERIOD_FORMATTER = new PeriodFormatterBuilder() 26 | .appendHours() 27 | .appendSuffix(" h ") 28 | .printZeroAlways() 29 | .minimumPrintedDigits(2) 30 | .appendMinutes() 31 | .appendSuffix(" mins ") 32 | .toFormatter(); 33 | 34 | @Getter 35 | private final UserDetails user; 36 | 37 | private final YearMonth yearMonth; 38 | 39 | private Map> dateTimeEntriesMap = new TreeMap<>(); 40 | 41 | private Map cachedStatistics = null; 42 | 43 | private WeekStatistics cachedWeekStatistics = null; 44 | 45 | public static double roundedHours(long milliseconds) { 46 | return Math.round(((double) milliseconds / 1000.0 / 60.0 / 60.0) * 10.0) / 10.0; 47 | } 48 | 49 | public UserTimeline addTimeEntries(LocalDate localDate, List entries) { 50 | dateTimeEntriesMap.put(localDate, entries); 51 | return this; 52 | } 53 | 54 | public long totalMillisecondsWorked() { 55 | AtomicLong totalMilliseconds = new AtomicLong(0); 56 | dateTimeEntriesMap.values() 57 | .forEach(v -> { 58 | totalMilliseconds.addAndGet(v.stream() 59 | .mapToLong(e -> e.getDuration()) 60 | .sum()); 61 | }); 62 | return totalMilliseconds.get(); 63 | } 64 | 65 | public String getTotalHoursFormatted() { 66 | return UserTimeline.PERIOD_FORMATTER.print(new Period(totalMillisecondsWorked())); 67 | } 68 | 69 | public List getHoursWorked() { 70 | List result = new ArrayList<>(); 71 | 72 | List dateSeries = YearMonthUtil.getAllDatesOfMonth(yearMonth); 73 | dateSeries.forEach(day -> { 74 | if (dateTimeEntriesMap.containsKey(day)) { 75 | // duration is in milliseconds 76 | result.add(roundedHours(dateTimeEntriesMap.get(day) 77 | .stream() 78 | .mapToLong(e -> e.getDuration()) 79 | .sum())); 80 | } else { 81 | result.add(null); 82 | } 83 | }); 84 | return result; 85 | } 86 | 87 | public Map getWeekStatisticsOfMonth() { 88 | if (cachedStatistics == null) { 89 | Map> weekTimeEntryList = new TreeMap<>(); 90 | 91 | dateTimeEntriesMap.forEach((date, entries) -> { 92 | weekTimeEntryList.putIfAbsent(date.getWeekOfWeekyear(), new ArrayList<>()); 93 | weekTimeEntryList.get(date.getWeekOfWeekyear()) 94 | .addAll(entries); 95 | }); 96 | 97 | Map result = new TreeMap<>(); 98 | 99 | Iterator>> iterator = weekTimeEntryList.entrySet() 100 | .iterator(); 101 | while (iterator.hasNext()) { 102 | Entry> entry = iterator.next(); 103 | result.put(entry.getKey(), buildWeekStatistics(entry.getKey(), entry.getValue())); 104 | } 105 | 106 | cachedStatistics = result; 107 | } 108 | return cachedStatistics; 109 | } 110 | 111 | private WeekStatistics buildWeekStatistics(int weekOfWeekyear, List timeEntries) { 112 | double totalHours = roundedHours(timeEntries 113 | .stream() 114 | .mapToLong(v -> v.getDuration()) 115 | .sum()); 116 | 117 | int workedDays = timeEntries 118 | .stream() 119 | .map(v -> v.getStart() 120 | .toLocalDate()) 121 | .collect(Collectors.toSet()) 122 | .size(); 123 | 124 | double billableHours = roundedHours(timeEntries 125 | .stream() 126 | .filter(v -> v.getBillable() != null && v.getBillable()) 127 | .mapToLong(v -> v.getDuration()) 128 | .sum()); 129 | 130 | long billableAmount = timeEntries 131 | .stream() 132 | .filter(v -> v.getBillable() != null && v.getBillable()) 133 | .mapToLong(v -> v.getBillableAmount()) 134 | .sum(); 135 | 136 | return new WeekStatistics(weekOfWeekyear, totalHours, workedDays, billableHours, billableAmount); 137 | } 138 | 139 | public WeekStatistics getWeekStatisticsOfWeek(YearWeek yearWeek) { 140 | if (cachedWeekStatistics == null) { 141 | List dateSeries = YearWeekUtil.getAllDatesOfYearWeek(yearWeek); 142 | List timeEntries = new ArrayList<>(); 143 | dateSeries.forEach(day -> { 144 | if (dateTimeEntriesMap.containsKey(day)) { 145 | timeEntries.addAll(dateTimeEntriesMap.get(day)); 146 | } 147 | }); 148 | cachedWeekStatistics = buildWeekStatistics(yearWeek.getWeek(), timeEntries); 149 | } 150 | return cachedWeekStatistics; 151 | } 152 | 153 | @Getter 154 | @RequiredArgsConstructor 155 | public static class WeekStatistics { 156 | private final int weekOfWeekyear; 157 | 158 | private final double totalHours; 159 | 160 | private final int workedDays; 161 | 162 | private final double billableHours; 163 | 164 | private final long billableAmount; 165 | 166 | public double getAverageHoursPerDay() { 167 | return Math.round((totalHours / (double) workedDays) * 10.0) / 10.0; 168 | } 169 | 170 | } 171 | 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/config/TogglService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.config; 2 | 3 | import ch.simas.jtoggl.JToggl; 4 | import ch.simas.jtoggl.domain.User; 5 | import ch.simas.jtoggl.domain.Workspace; 6 | import com.timgroup.jgravatar.Gravatar; 7 | import com.timgroup.jgravatar.GravatarDefaultImage; 8 | import de.jollyday.HolidayCalendar; 9 | import io.rocketbase.toggl.api.TogglReportApi; 10 | import io.rocketbase.toggl.api.TogglReportApiBuilder; 11 | import io.rocketbase.toggl.backend.model.ApplicationSetting; 12 | import io.rocketbase.toggl.backend.model.ApplicationSetting.SchedulingConfig; 13 | import io.rocketbase.toggl.backend.model.ApplicationSetting.UserDetails; 14 | import io.rocketbase.toggl.backend.repository.ApplicationSettingRepository; 15 | import io.rocketbase.toggl.backend.util.ColorPalette; 16 | import lombok.Getter; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.StringUtils; 19 | 20 | import javax.annotation.PostConstruct; 21 | import javax.annotation.Resource; 22 | import java.time.DayOfWeek; 23 | import java.util.*; 24 | 25 | /** 26 | * Created by marten on 09.03.17. 27 | */ 28 | @Component 29 | public class TogglService implements TogglReportApiBuilder.WorkspaceProvider { 30 | 31 | public static final Gravatar GRAVATAR = new Gravatar(64, Gravatar.DEFAULT_RATING, GravatarDefaultImage.IDENTICON); 32 | 33 | @Resource 34 | private ApplicationSettingRepository applicationSettingRepository; 35 | 36 | private ApplicationSetting applicationSettings; 37 | 38 | @Getter 39 | private JToggl jToggl; 40 | 41 | @Getter 42 | private TogglReportApi togglReportApi; 43 | 44 | @PostConstruct 45 | public void init() { 46 | List entities = applicationSettingRepository.findAll(); 47 | if (entities == null || entities.size() <= 0) { 48 | applicationSettings = applicationSettingRepository.save(ApplicationSetting.builder() 49 | .currentWorkspaceId(-1) 50 | .workspaceMap(new HashMap<>()) 51 | .userMap(new HashMap<>()) 52 | .build()); 53 | } else { 54 | applicationSettings = entities.get(0); 55 | } 56 | initApis(); 57 | } 58 | 59 | @Override 60 | public long getWorkspaceId() { 61 | return applicationSettings.getCurrentWorkspaceId(); 62 | } 63 | 64 | public void setWorkspace(Workspace workspace) { 65 | if (workspace != null && applicationSettings.getCurrentWorkspaceId() != workspace.getId()) { 66 | applicationSettings.setCurrentWorkspaceId(workspace.getId()); 67 | applicationSettings.getWorkspaceMap() 68 | .putIfAbsent(workspace.getId(), workspace); 69 | 70 | updateCurrentWorkspaceUsers(); 71 | } 72 | } 73 | 74 | public void updateCurrentWorkspaceUsers() { 75 | List workspaceUsers = jToggl.getWorkspaceUsers(getWorkspaceId()); 76 | if (workspaceUsers != null) { 77 | workspaceUsers.forEach(u -> { 78 | if (applicationSettings.getUserMap() 79 | .containsKey(u.getId())) { 80 | UserDetails userDetails = applicationSettings.getUserMap() 81 | .get(u.getId()); 82 | userDetails.setAvatar(GRAVATAR.getUrl(u.getEmail())); 83 | } else { 84 | UserDetails userDetails = new UserDetails(u.getId(), u.getFullname(), u.getEmail()); 85 | userDetails.setGraphColor(ColorPalette.getRandomValue()); 86 | userDetails.setAvatar(GRAVATAR.getUrl(u.getEmail())); 87 | applicationSettings.getUserMap() 88 | .put(u.getId(), userDetails); 89 | } 90 | }); 91 | } 92 | applicationSettingRepository.save(applicationSettings); 93 | } 94 | 95 | 96 | public boolean isApiTokenAvailable() { 97 | return !StringUtils.isEmpty(applicationSettings.getApiToken()); 98 | } 99 | 100 | public void updateToken(String apiToken) { 101 | applicationSettings.setApiToken(apiToken); 102 | applicationSettingRepository.save(applicationSettings); 103 | 104 | initApis(); 105 | 106 | // in order to fire check 107 | try { 108 | jToggl.getCurrentUser(); 109 | } catch (Exception e) { 110 | applicationSettings.setApiToken(null); 111 | applicationSettingRepository.save(applicationSettings); 112 | throw new RuntimeException("invalid api-token"); 113 | } 114 | } 115 | 116 | private void initApis() { 117 | togglReportApi = new TogglReportApiBuilder() 118 | .apiToken(applicationSettings.getApiToken()) 119 | .userAgent("toggl-reporter") 120 | .workspaceProvider(this) 121 | .build(); 122 | 123 | jToggl = new JToggl(applicationSettings.getApiToken()); 124 | } 125 | 126 | public Workspace getWorkspaceById(long workspaceId) { 127 | return applicationSettings.getWorkspaceMap() 128 | .getOrDefault(workspaceId, new Workspace()); 129 | } 130 | 131 | public UserDetails getUserById(long userId) { 132 | return applicationSettings.getUserMap() 133 | .getOrDefault(userId, new UserDetails(userId, "", "")); 134 | } 135 | 136 | public List getAllUsers() { 137 | return new ArrayList<>(applicationSettings.getUserMap() 138 | .values()); 139 | } 140 | 141 | public void updateUser(UserDetails user) { 142 | applicationSettings.getUserMap() 143 | .put(user.getUid(), user); 144 | applicationSettingRepository.save(applicationSettings); 145 | } 146 | 147 | public HolidayCalendar getHolidayCalender() { 148 | return applicationSettings.getHolidayCalendar(); 149 | } 150 | 151 | 152 | public void updateHolidayCalendar(HolidayCalendar holidayCalendar) { 153 | applicationSettings.setHolidayCalendar(holidayCalendar); 154 | applicationSettingRepository.save(applicationSettings); 155 | } 156 | 157 | public void updateRegularWorkinsDays(Set dayOfWeeks) { 158 | applicationSettings.setRegularWorkinsDays(dayOfWeeks); 159 | applicationSettingRepository.save(applicationSettings); 160 | } 161 | 162 | public Set getRegularWorkinsDays() { 163 | return applicationSettings.getRegularWorkinsDays() == null ? Collections.emptySet() : applicationSettings.getRegularWorkinsDays(); 164 | } 165 | 166 | public SchedulingConfig getSchedulingConfig() { 167 | return applicationSettings.getSchedulingConfig(); 168 | } 169 | 170 | 171 | public void updateSchedulingConfig(SchedulingConfig schedulingConfig) { 172 | applicationSettings.setSchedulingConfig(schedulingConfig); 173 | applicationSettingRepository.save(applicationSettings); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/backend/service/TimeEntryService.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.backend.service; 2 | 3 | import io.rocketbase.toggl.backend.config.TogglService; 4 | import io.rocketbase.toggl.backend.model.DateTimeEntryGroup; 5 | import io.rocketbase.toggl.backend.model.report.UserTimeline; 6 | import io.rocketbase.toggl.backend.model.report.WeekTimeline; 7 | import io.rocketbase.toggl.backend.repository.DateTimeEntryGroupRepository; 8 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 9 | import io.rocketbase.toggl.backend.util.YearWeekUtil; 10 | import org.joda.time.LocalDate; 11 | import org.joda.time.YearMonth; 12 | import org.springframework.data.domain.PageRequest; 13 | import org.springframework.data.domain.Sort.Direction; 14 | import org.springframework.data.mongodb.core.MongoTemplate; 15 | import org.springframework.data.mongodb.core.query.Criteria; 16 | import org.springframework.data.mongodb.core.query.Query; 17 | import org.springframework.stereotype.Service; 18 | import org.threeten.extra.YearWeek; 19 | 20 | import javax.annotation.Resource; 21 | import javax.validation.constraints.NotNull; 22 | import java.util.*; 23 | import java.util.stream.Collectors; 24 | 25 | /** 26 | * Created by marten on 09.03.17. 27 | */ 28 | @Service 29 | public class TimeEntryService { 30 | 31 | @Resource 32 | private DateTimeEntryGroupRepository dateTimeEntryGroupRepository; 33 | 34 | @Resource 35 | private TogglService togglService; 36 | 37 | @Resource 38 | private MongoTemplate mongoTemplate; 39 | 40 | @Resource 41 | private HolidayManagerService holidayManagerService; 42 | 43 | public int countAll() { 44 | return (int) dateTimeEntryGroupRepository.count(); 45 | } 46 | 47 | public List findPaged(int page, int perPage) { 48 | return dateTimeEntryGroupRepository.findAll(new PageRequest(page, perPage, Direction.DESC, "date")) 49 | .getContent(); 50 | } 51 | 52 | public List getCurrentMonth() { 53 | return dateTimeEntryGroupRepository.findByWorkspaceIdAndDateBetween(togglService.getWorkspaceId(), 54 | LocalDate.now() 55 | .withDayOfMonth(1) 56 | .minusDays(1) 57 | .toDate(), 58 | LocalDate.now() 59 | .plusMonths(1) 60 | .withDayOfMonth(1) 61 | .toDate()); 62 | } 63 | 64 | public List fetchAllYearMonths() { 65 | Query query = Query.query(Criteria.where("workspaceId") 66 | .is(togglService.getWorkspaceId())); 67 | query.fields() 68 | .include("date"); 69 | 70 | List queryResult = mongoTemplate.find(query, DateTimeEntryGroup.class, DateTimeEntryGroup.COLLECTION_NAME); 71 | 72 | Set result = new TreeSet<>(); 73 | queryResult.forEach(e -> result.add(new YearMonth(e.getDate()))); 74 | 75 | return result.stream() 76 | .sorted(Comparator.reverseOrder()) 77 | .collect(Collectors.toList()); 78 | } 79 | 80 | public List getUserTimeLines(YearMonth yearMonth) { 81 | List queryResult = dateTimeEntryGroupRepository.findByWorkspaceIdAndDateBetween(togglService.getWorkspaceId(), 82 | yearMonth.toLocalDate(1) 83 | .minusDays(1) 84 | .toDate(), 85 | yearMonth.toLocalDate(1) 86 | .plusMonths(1) 87 | .toDate()); 88 | 89 | Map result = new HashMap<>(); 90 | queryResult.forEach(e -> { 91 | e.getUserTimeEntriesMap() 92 | .forEach((userId, timeEntries) -> { 93 | result.putIfAbsent(userId, new UserTimeline(togglService.getUserById(userId), yearMonth)); 94 | result.get(userId) 95 | .addTimeEntries(e.getDate(), timeEntries); 96 | }); 97 | }); 98 | return new ArrayList<>(result.values()); 99 | } 100 | 101 | public List fetchAllYearWeeks() { 102 | Query query = Query.query(Criteria.where("workspaceId") 103 | .is(togglService.getWorkspaceId())); 104 | query.fields() 105 | .include("date"); 106 | 107 | List queryResult = mongoTemplate.find(query, DateTimeEntryGroup.class, DateTimeEntryGroup.COLLECTION_NAME); 108 | 109 | Set result = new TreeSet<>(); 110 | queryResult.forEach(e -> result.add(YearWeek.of(e.getDate() 111 | .getYear(), 112 | e.getDate() 113 | .getWeekOfWeekyear()))); 114 | 115 | return result.stream() 116 | .sorted(Comparator.reverseOrder()) 117 | .collect(Collectors.toList()); 118 | } 119 | 120 | 121 | public List getWeekTimelines(@NotNull YearWeek from, YearWeek to) { 122 | List queryResult = dateTimeEntryGroupRepository.findByWorkspaceIdAndDateBetween(togglService.getWorkspaceId(), 123 | YearWeekUtil.getFirstDay(from) 124 | .minusDays(1) 125 | .toDate(), 126 | to != null ? YearWeekUtil.getLastDay(to) 127 | .plusDays(1) 128 | .toDate() : LocalDate.now() 129 | .plusDays(1) 130 | .toDate()); 131 | 132 | Map> result = new HashMap<>(); 133 | queryResult.forEach(e -> { 134 | YearMonth yearMonth = YearMonth.fromDateFields(e.getDate() 135 | .toDate()); 136 | e.getUserTimeEntriesMap() 137 | .forEach((userId, timeEntries) -> { 138 | YearWeek yearWeek = YearWeek.of(e.getDate() 139 | .getYear(), 140 | e.getDate() 141 | .getWeekOfWeekyear()); 142 | result.putIfAbsent(yearWeek, new HashMap<>()); 143 | Map yearWeekMap = result.get(yearWeek); 144 | yearWeekMap.putIfAbsent(userId, new UserTimeline(togglService.getUserById(userId), yearMonth)); 145 | yearWeekMap.get(userId) 146 | .addTimeEntries(e.getDate(), timeEntries); 147 | }); 148 | }); 149 | return result.entrySet() 150 | .stream() 151 | .map(e -> { 152 | WeekTimeline r = new WeekTimeline(e.getKey()); 153 | r.getUidTimelines() 154 | .putAll(e.getValue()); 155 | r.getHolidays() 156 | .addAll(holidayManagerService.getHolidays(LocalDateConverter.convert(YearWeekUtil.getFirstDay(e.getKey())), 157 | LocalDateConverter.convert(YearWeekUtil.getLastDay(e.getKey())))); 158 | return r; 159 | }) 160 | .sorted(Comparator.comparing(WeekTimeline::getYearWeek) 161 | .reversed()) 162 | .collect(Collectors.toList()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/home/tab/MonthStatisticsTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.home.tab; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.vaadin.server.ExternalResource; 5 | import com.vaadin.shared.ui.ContentMode; 6 | import com.vaadin.spring.annotation.SpringComponent; 7 | import com.vaadin.spring.annotation.UIScope; 8 | import com.vaadin.ui.*; 9 | import de.jollyday.Holiday; 10 | import io.rocketbase.toggl.backend.model.report.UserTimeline; 11 | import io.rocketbase.toggl.backend.service.HolidayManagerService; 12 | import io.rocketbase.toggl.backend.service.TimeEntryService; 13 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 14 | import io.rocketbase.toggl.backend.util.YearMonthUtil; 15 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 16 | import io.rocketbase.toggl.ui.view.home.HomeView; 17 | import org.joda.time.YearMonth; 18 | import org.vaadin.viritin.MSize; 19 | import org.vaadin.viritin.label.MLabel; 20 | import org.vaadin.viritin.layouts.MHorizontalLayout; 21 | import org.vaadin.viritin.layouts.MVerticalLayout; 22 | 23 | import javax.annotation.Resource; 24 | import java.time.format.DateTimeFormatter; 25 | import java.util.ArrayList; 26 | import java.util.Comparator; 27 | import java.util.List; 28 | import java.util.Set; 29 | import java.util.stream.Collectors; 30 | 31 | 32 | @UIScope 33 | @SpringComponent 34 | public class MonthStatisticsTab extends AbstractTab { 35 | 36 | @Resource 37 | private TimeEntryService timeEntryService; 38 | 39 | @Resource 40 | private HolidayManagerService holidayManagerService; 41 | 42 | private MVerticalLayout layout; 43 | 44 | private ComboBox typedSelect = new ComboBox<>(); 45 | 46 | 47 | @Override 48 | public Component initLayout() { 49 | layout = new MVerticalLayout() 50 | .withSize(MSize.FULL_SIZE) 51 | .withMargin(false) 52 | .add(HomeView.getPlaceHolder(), 1); 53 | 54 | 55 | typedSelect.setEmptySelectionAllowed(false); 56 | typedSelect.setTextInputAllowed(false); 57 | typedSelect.setWidth("100%"); 58 | typedSelect.addValueChangeListener(e -> filter()); 59 | 60 | return new MVerticalLayout() 61 | .add(new MHorizontalLayout() 62 | .add(typedSelect, Alignment.MIDDLE_RIGHT) 63 | .withFullWidth()) 64 | .add(layout, 1) 65 | .withSize(MSize.FULL_SIZE); 66 | } 67 | 68 | @Override 69 | public void onTabEnter() { 70 | layout.removeAllComponents(); 71 | List items = timeEntryService.fetchAllYearMonths(); 72 | typedSelect.setItems(items); 73 | if (typedSelect.getValue() == null && items != null && !items.isEmpty()) { 74 | typedSelect.setValue(items.get(0)); 75 | } 76 | filter(); 77 | } 78 | 79 | private void filter() { 80 | layout.removeAllComponents(); 81 | if (typedSelect.getValue() != null) { 82 | layout.add(genTable(typedSelect.getValue()), 1); 83 | layout.add(genHolidays(typedSelect.getValue())); 84 | } else { 85 | layout.add(HomeView.getPlaceHolder(), 1); 86 | } 87 | } 88 | 89 | private Component genHolidays(YearMonth filter) { 90 | Set holidaySet = holidayManagerService.getHolidays(filter); 91 | 92 | TextArea textArea = new TextArea("Holidays", Joiner.on(",\t") 93 | .join(holidaySet.stream() 94 | .sorted(Comparator.comparing(Holiday::getDate)) 95 | .map(h -> String.format("%s (Week: %d): %s", 96 | h.getDate() 97 | .format(DateTimeFormatter.ISO_DATE), 98 | LocalDateConverter.convert(h.getDate()) 99 | .getWeekOfWeekyear(), 100 | h.getDescription(UI.getCurrent() 101 | .getLocale()))) 102 | .collect(Collectors.toList()))); 103 | textArea.setVisible(holidaySet.size() > 0); 104 | textArea.setWidth("100%"); 105 | textArea.setHeight("50px"); 106 | return textArea; 107 | } 108 | 109 | protected Component genTable(YearMonth yearMonth) { 110 | List userTimelineList = timeEntryService.getUserTimeLines(yearMonth); 111 | 112 | Grid grid = new Grid<>(null, userTimelineList.stream() 113 | .sorted(Comparator.comparing(UserTimeline::totalMillisecondsWorked) 114 | .reversed()) 115 | .collect(Collectors.toList())); 116 | grid.setSizeFull(); 117 | grid.setBodyRowHeight(150); 118 | 119 | grid.addComponentColumn(e -> { 120 | Image avatar = new Image(null, 121 | new ExternalResource(e.getUser() 122 | .getAvatar())); 123 | avatar.setWidth("64px"); 124 | avatar.setHeight("64px"); 125 | 126 | return new MHorizontalLayout() 127 | .add(avatar, Alignment.MIDDLE_CENTER) 128 | .add(new MVerticalLayout() 129 | .withMargin(false) 130 | .add(genLabelInfo("Name", 131 | e.getUser() 132 | .getName())) 133 | .add(genLabelInfo("Total", e.getTotalHoursFormatted()).withStyleName("left-right")) 134 | .withFullWidth(), Alignment.MIDDLE_LEFT, 1) 135 | .withHeight("150px") 136 | .withFullWidth() 137 | .withStyleName("cell-content-wrapper"); 138 | }) 139 | .setCaption("user") 140 | .setWidth(300); 141 | 142 | List weekList = YearMonthUtil.getAllWeeksOfWeekyear(yearMonth); 143 | weekList.forEach(week -> { 144 | grid.addComponentColumn(e -> { 145 | if (e.getWeekStatisticsOfMonth() 146 | .containsKey(week)) { 147 | UserTimeline.WeekStatistics stat = e.getWeekStatisticsOfMonth() 148 | .get(week); 149 | return new MVerticalLayout() 150 | .withFullWidth() 151 | .withMargin(false) 152 | .withSpacing(false) 153 | .add(genLabelInfo("Total hours", stat.getTotalHours())) 154 | .add(genLabelInfo("Billable hours", stat.getBillableHours())) 155 | .add(genLabelInfo("Earned", stat.getBillableAmount())) 156 | .add(genLabelInfo("Days worked", stat.getWorkedDays())) 157 | .add(genLabelInfo("Ø per day", stat.getAverageHoursPerDay())) 158 | .withStyleName("cell-content-wrapper"); 159 | } else { 160 | return null; 161 | } 162 | }) 163 | .setCaption(String.valueOf(week)) 164 | .setWidth(190); 165 | }); 166 | 167 | List properties = new ArrayList<>(); 168 | properties.add("user"); 169 | properties.addAll(weekList.stream() 170 | .map(v -> String.valueOf(v)) 171 | .collect(Collectors.toList())); 172 | 173 | return grid; 174 | } 175 | 176 | private MLabel genLabelInfo(String caption, Number value) { 177 | return genLabelInfo(caption, String.valueOf(value)).withStyleName("left-right"); 178 | } 179 | 180 | private MLabel genLabelInfo(String caption, String value) { 181 | return new MLabel(String.format("%s: %s", caption, value)).withFullWidth() 182 | .withContentMode(ContentMode.HTML) 183 | .withStyleName("info"); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/setting/tab/PullDataTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.setting.tab; 2 | 3 | import ch.simas.jtoggl.domain.Workspace; 4 | import com.vaadin.data.ValueProvider; 5 | import com.vaadin.icons.VaadinIcons; 6 | import com.vaadin.spring.annotation.SpringComponent; 7 | import com.vaadin.spring.annotation.UIScope; 8 | import com.vaadin.ui.*; 9 | import io.rocketbase.toggl.backend.config.TogglService; 10 | import io.rocketbase.toggl.backend.model.DateTimeEntryGroup; 11 | import io.rocketbase.toggl.backend.model.report.UserTimeline; 12 | import io.rocketbase.toggl.backend.service.FetchAndStoreService; 13 | import io.rocketbase.toggl.backend.service.TimeEntryService; 14 | import io.rocketbase.toggl.backend.util.LocalDateConverter; 15 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 16 | import org.joda.time.Period; 17 | import org.joda.time.format.DateTimeFormat; 18 | import org.joda.time.format.DateTimeFormatter; 19 | import org.vaadin.viritin.MSize; 20 | import org.vaadin.viritin.button.MButton; 21 | import org.vaadin.viritin.grid.MGrid; 22 | import org.vaadin.viritin.label.MLabel; 23 | import org.vaadin.viritin.layouts.MHorizontalLayout; 24 | import org.vaadin.viritin.layouts.MVerticalLayout; 25 | import org.vaadin.viritin.layouts.MWindow; 26 | 27 | import javax.annotation.Resource; 28 | import java.util.List; 29 | import java.util.concurrent.atomic.AtomicLong; 30 | 31 | /** 32 | * Created by marten on 09.03.17. 33 | */ 34 | @UIScope 35 | @SpringComponent 36 | public class PullDataTab extends AbstractTab { 37 | 38 | private static final int PER_PAGE = 45; 39 | 40 | private static DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); 41 | 42 | @Resource 43 | private TogglService togglService; 44 | 45 | @Resource 46 | private FetchAndStoreService fetchAndStoreService; 47 | 48 | @Resource 49 | private TimeEntryService timeEntryService; 50 | 51 | private MGrid dateTimeGrid; 52 | 53 | @Override 54 | public Component initLayout() { 55 | dateTimeGrid = initFetchedDataTable(); 56 | 57 | return new MVerticalLayout() 58 | .add(initFetchSelection()) 59 | .add(dateTimeGrid, 1) 60 | .withSize(MSize.FULL_SIZE); 61 | } 62 | 63 | @Override 64 | public void onTabEnter() { 65 | dateTimeGrid.setDataProvider((sortOrders, offset, limit) -> 66 | timeEntryService.findPaged(offset / PER_PAGE, PER_PAGE) 67 | .stream(), 68 | () -> timeEntryService.countAll()); 69 | } 70 | 71 | private ComboBox initWorkspaceSelect() { 72 | List workspaces = togglService.getJToggl() 73 | .getWorkspaces(); 74 | 75 | ComboBox workspaceTypedSelect = new ComboBox("workspace", workspaces); 76 | workspaceTypedSelect.setItemCaptionGenerator(w -> w.getName()); 77 | workspaceTypedSelect.setEmptySelectionAllowed(false); 78 | workspaceTypedSelect.setWidth("100%"); 79 | workspaceTypedSelect.addValueChangeListener(e -> togglService.setWorkspace(e.getValue())); 80 | 81 | workspaces.forEach(w -> { 82 | if (w.getId() 83 | .equals(togglService.getWorkspaceId())) { 84 | workspaceTypedSelect.setValue(w); 85 | } 86 | }); 87 | 88 | return workspaceTypedSelect; 89 | } 90 | 91 | 92 | private MHorizontalLayout initFetchSelection() { 93 | ComboBox workspaceTypedSelect = initWorkspaceSelect(); 94 | boolean workspaceSelected = workspaceTypedSelect.getValue() != null; 95 | 96 | DateField from = new DateField("from"); 97 | from.setWidth("100%"); 98 | from.setEnabled(workspaceSelected); 99 | DateField to = new DateField("to"); 100 | to.setWidth("100%"); 101 | to.setEnabled(workspaceSelected); 102 | 103 | MButton fetch = new MButton(VaadinIcons.DOWNLOAD, "fetch", e -> { 104 | if (from.getValue() != null && to.getValue() != null) { 105 | final UI ui = UI.getCurrent(); 106 | ui.setPollInterval(100); 107 | 108 | MWindow waitWindow = initWaitWindow(); 109 | ui.addWindow(waitWindow); 110 | 111 | new Thread(() -> { 112 | UI.setCurrent(ui); 113 | fetchAndStoreService.fetchBetween(LocalDateConverter.convert(from.getValue()), LocalDateConverter.convert(to.getValue())); 114 | 115 | ui.access(() -> { 116 | waitWindow.close(); 117 | refreshTab(); 118 | UI.getCurrent() 119 | .setPollInterval(-1); 120 | }); 121 | }).start(); 122 | } else { 123 | Notification.show("Please select from and to"); 124 | } 125 | }); 126 | fetch.setEnabled(workspaceSelected); 127 | 128 | workspaceTypedSelect.addValueChangeListener(e -> { 129 | from.setEnabled(e != null); 130 | to.setEnabled(e != null); 131 | fetch.setEnabled(e != null); 132 | }); 133 | 134 | return new MHorizontalLayout() 135 | .add(workspaceTypedSelect, 1) 136 | .add(from, 1) 137 | .add(to, 1) 138 | .add(fetch, Alignment.BOTTOM_CENTER) 139 | .withFullWidth(); 140 | } 141 | 142 | private MWindow initWaitWindow() { 143 | ProgressBar progressBar = new ProgressBar(); 144 | progressBar.setIndeterminate(true); 145 | return new MWindow("Fetching Data...").withContent(new MVerticalLayout().withSize(MSize.FULL_SIZE) 146 | .add(progressBar, Alignment.MIDDLE_CENTER)) 147 | .withModal(true) 148 | .withDraggable(false) 149 | .withClosable(false) 150 | .withResizable(false) 151 | .withWidth("200px") 152 | .withHeight("200px") 153 | .withCenter(); 154 | } 155 | 156 | private MGrid initFetchedDataTable() { 157 | MGrid grid = new MGrid<>(DateTimeEntryGroup.class) 158 | .withProperties() 159 | .withSize(MSize.FULL_SIZE); 160 | grid.addComponentColumn((ValueProvider) bean -> new MLabel(togglService.getWorkspaceById(bean.getWorkspaceId()) 161 | .getName())) 162 | .setCaption("workspace"); 163 | grid.addColumn("date") 164 | .setCaption("date"); 165 | grid.addComponentColumn((ValueProvider) bean -> 166 | new MLabel(bean.getFetched() != null ? bean.getFetched() 167 | .toString(DATE_TIME_FORMAT) : "-")) 168 | .setCaption("fetched"); 169 | grid.addComponentColumn((ValueProvider) bean -> new MLabel(String.valueOf(bean.getUserTimeEntriesMap() 170 | .size()))) 171 | .setCaption("count of users"); 172 | grid.addComponentColumn((ValueProvider) bean -> { 173 | AtomicLong totalMilliseconds = new AtomicLong(0); 174 | bean.getUserTimeEntriesMap() 175 | .forEach((user, timeEntries) -> { 176 | totalMilliseconds.addAndGet(timeEntries.stream() 177 | .mapToLong(e -> e.getDuration()) 178 | .sum()); 179 | }); 180 | return new MLabel(UserTimeline.PERIOD_FORMATTER.print(new Period(totalMilliseconds.get()))); 181 | }) 182 | .setCaption("total time"); 183 | 184 | grid.setColumnReorderingAllowed(false); 185 | grid.getColumns() 186 | .forEach(c -> c.setSortable(false)); 187 | return grid; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.rocketbase.toggl 7 | toggl-reporter 8 | 1.1.1-SNAPSHOT 9 | jar 10 | 11 | toggl-reporter 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.9.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | false 26 | 27 | 8.2.1 28 | 3.0.0 29 | 2.1 30 | 31 | jtoggl-api-8.0.0 32 | toggl-report-api-1.0.1 33 | 1.1.1 34 | 35 | 0.5.2 36 | 1.1 37 | 38 | rocketbaseio 39 | 40 | 1.3.7 41 | 42 | 43 | 44 | 45 | scm:git:git@github.com:rocketbase-io/toggl-reporter.git 46 | scm:git:git@github.com:rocketbase-io/toggl-reporter.git 47 | https://github.com/rocketbase-io/toggl-reporter.git 48 | HEAD 49 | 50 | 51 | 52 | 53 | vaadin-addons 54 | http://maven.vaadin.com/vaadin-addons 55 | 56 | 57 | 58 | jitpack.io 59 | https://jitpack.io 60 | 61 | 62 | 63 | 64 | 65 | 66 | com.vaadin 67 | vaadin-bom 68 | ${vaadin.version} 69 | pom 70 | import 71 | 72 | 73 | 74 | 75 | 76 | 77 | com.vaadin 78 | vaadin-spring-boot-starter 79 | ${vaadin-spring-boot.version} 80 | 81 | 82 | com.vaadin 83 | vaadin-client-compiled 84 | 85 | 86 | 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-starter-data-mongodb 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-starter-security 96 | 97 | 98 | 99 | org.springframework.boot 100 | spring-boot-starter-thymeleaf 101 | 102 | 103 | 104 | de.jollyday 105 | jollyday 106 | ${jollyday.version} 107 | 108 | 109 | 110 | org.springframework.boot 111 | spring-boot-starter-test 112 | test 113 | 114 | 115 | 116 | com.timgroup 117 | jgravatar 118 | ${jgravatar.version} 119 | 120 | 121 | 122 | org.projectlombok 123 | lombok 124 | 125 | 126 | 127 | com.github.konikvranik 128 | jtoggl 129 | ${jtoggl.version} 130 | 131 | 132 | 133 | com.github.rocketbase-io 134 | toggl-report-api 135 | ${toggl-report-api.version} 136 | 137 | 138 | 139 | 140 | com.vaadin 141 | vaadin-server 142 | 143 | 144 | com.vaadin 145 | vaadin-themes 146 | 147 | 148 | com.vaadin 149 | vaadin-client-compiled 150 | 151 | 152 | 153 | org.vaadin 154 | viritin 155 | ${viritin.version} 156 | 157 | 158 | com.byteowls 159 | vaadin-chartjs 160 | ${vaadin-chartjs.version} 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | com.spotify 171 | dockerfile-maven-extension 172 | ${dockerfile-maven-plugin.version} 173 | 174 | 175 | 176 | 177 | 178 | org.springframework.boot 179 | spring-boot-maven-plugin 180 | 181 | 182 | 183 | 184 | com.spotify 185 | dockerfile-maven-plugin 186 | ${dockerfile-maven-plugin.version} 187 | 188 | 189 | default 190 | 191 | push 192 | 193 | 194 | 195 | 196 | rocketbaseio/toggl-reporter 197 | 198 | ${project.version} 199 | latest 200 | 201 | 202 | ${project.build.finalName}.jar 203 | 204 | true 205 | 206 | 207 | 208 | 209 | org.apache.maven.plugins 210 | maven-javadoc-plugin 211 | 2.10.4 212 | 213 | 214 | attach-javadocs 215 | 216 | jar 217 | 218 | 219 | -Xdoclint:none 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/component/Menu.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.component; 2 | 3 | import com.vaadin.icons.VaadinIcons; 4 | import com.vaadin.server.FontIcon; 5 | import com.vaadin.server.ThemeResource; 6 | import com.vaadin.spring.annotation.SpringComponent; 7 | import com.vaadin.spring.annotation.SpringView; 8 | import com.vaadin.spring.annotation.UIScope; 9 | import com.vaadin.ui.*; 10 | import com.vaadin.ui.Button.ClickListener; 11 | import com.vaadin.ui.themes.ValoTheme; 12 | import io.rocketbase.toggl.backend.security.MongoUserDetails; 13 | import io.rocketbase.toggl.backend.security.MongoUserService; 14 | import io.rocketbase.toggl.backend.security.UserRole; 15 | import io.rocketbase.toggl.ui.view.AbstractView; 16 | import lombok.Getter; 17 | import lombok.RequiredArgsConstructor; 18 | import lombok.Setter; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.context.ApplicationContext; 21 | import org.springframework.security.core.context.SecurityContextHolder; 22 | import org.vaadin.viritin.button.PrimaryButton; 23 | import org.vaadin.viritin.layouts.MVerticalLayout; 24 | import org.vaadin.viritin.layouts.MWindow; 25 | 26 | import javax.annotation.PostConstruct; 27 | import javax.annotation.Resource; 28 | import java.util.*; 29 | import java.util.concurrent.atomic.AtomicInteger; 30 | 31 | @UIScope 32 | @SpringComponent 33 | public class Menu extends CssLayout { 34 | 35 | private static final String VALO_MENUITEMS = "valo-menuitems"; 36 | 37 | private static final String VALO_MENU_TOGGLE = "valo-menu-toggle"; 38 | 39 | private static final String VALO_MENU_VISIBLE = "valo-menu-visible"; 40 | 41 | @Value("${application.title}") 42 | private String applicationTitle; 43 | 44 | private Map viewMenus = new HashMap<>(); 45 | 46 | private CssLayout menuItemsLayout; 47 | 48 | private CssLayout menuPart; 49 | 50 | @Resource 51 | private ApplicationContext applicationContext; 52 | 53 | @Resource 54 | private MongoUserService mongoUserService; 55 | 56 | @Value("${development.mode.active:false}") 57 | private boolean developmentMode; 58 | 59 | @PostConstruct 60 | public void postConstruct() { 61 | setPrimaryStyleName(ValoTheme.MENU_ROOT); 62 | menuPart = new CssLayout(); 63 | menuPart.addStyleName(ValoTheme.MENU_PART); 64 | 65 | // header of the menu 66 | final HorizontalLayout top = new HorizontalLayout(); 67 | top.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT); 68 | top.addStyleName(ValoTheme.MENU_TITLE); 69 | top.setSpacing(true); 70 | Label title = new Label(applicationTitle); 71 | title.addStyleName(ValoTheme.LABEL_H3); 72 | title.setSizeUndefined(); 73 | Image image = new Image(null, new ThemeResource("img/toggl-logo.png")); 74 | image.setWidth(16, Unit.PIXELS); 75 | image.setHeight(16, Unit.PIXELS); 76 | image.setStyleName("logo"); 77 | top.addComponent(image); 78 | top.addComponent(title); 79 | menuPart.addComponent(top); 80 | 81 | // logout menu item 82 | menuPart.addComponent(initLogoutMenu()); 83 | 84 | // button for toggling the visibility of the menu when on a small screen 85 | final Button showMenu = new Button("Menu", (ClickListener) event -> { 86 | if (menuPart.getStyleName() 87 | .contains(VALO_MENU_VISIBLE)) { 88 | menuPart.removeStyleName(VALO_MENU_VISIBLE); 89 | } else { 90 | menuPart.addStyleName(VALO_MENU_VISIBLE); 91 | } 92 | }); 93 | showMenu.addStyleName(ValoTheme.BUTTON_PRIMARY); 94 | showMenu.addStyleName(ValoTheme.BUTTON_SMALL); 95 | showMenu.addStyleName(VALO_MENU_TOGGLE); 96 | showMenu.setIcon(VaadinIcons.MENU); 97 | menuPart.addComponent(showMenu); 98 | 99 | // container for the navigation buttons, which are added by addView() 100 | menuItemsLayout = new CssLayout(); 101 | menuItemsLayout.setPrimaryStyleName(VALO_MENUITEMS); 102 | 103 | scanViews(); 104 | 105 | menuPart.addComponent(menuItemsLayout); 106 | 107 | addComponent(menuPart); 108 | } 109 | 110 | private Component initLogoutMenu() { 111 | MenuBar logoutMenu = new MenuBar(); 112 | MenuBar.MenuItem logout = logoutMenu.addItem("", VaadinIcons.SIGN_OUT, (MenuBar.Command) selectedItem -> { 113 | UI.getCurrent() 114 | .getPage() 115 | .setLocation("logout"); 116 | }); 117 | logout.setDescription("logout"); 118 | MenuBar.MenuItem changePassword = logoutMenu.addItem("", VaadinIcons.KEY, (MenuBar.Command) selectedItem -> { 119 | PasswordField password = new PasswordField("password"); 120 | password.setWidth("100%"); 121 | 122 | MWindow window = new MWindow("change password") 123 | .withModal(true) 124 | .withDraggable(false) 125 | .withResizable(false) 126 | .withCenter(); 127 | window.setContent(new MVerticalLayout().add(password) 128 | .add(new PrimaryButton("change", changeEvent -> { 129 | mongoUserService.updatePassword(getLoggedInUser(), password.getValue()); 130 | Notification.show("successfully changed password"); 131 | window.close(); 132 | })) 133 | .withWidth("300px")); 134 | UI.getCurrent() 135 | .addWindow(window); 136 | }); 137 | changePassword.setDescription("change password"); 138 | logoutMenu.addStyleName("user-menu"); 139 | return logoutMenu; 140 | } 141 | 142 | private MongoUserDetails getLoggedInUser() { 143 | Object principal = SecurityContextHolder.getContext() 144 | .getAuthentication() 145 | .getPrincipal(); 146 | if (principal instanceof MongoUserDetails) { 147 | return (MongoUserDetails) principal; 148 | } 149 | return null; 150 | } 151 | 152 | private void scanViews() { 153 | List menuEntries = new ArrayList<>(); 154 | String[] viewBeanNames = applicationContext 155 | .getBeanNamesForAnnotation(SpringView.class); 156 | for (String beanName : viewBeanNames) { 157 | final Class type = applicationContext.getType(beanName); 158 | if (AbstractView.class.isAssignableFrom(type)) { 159 | AbstractView view = (AbstractView) applicationContext.getBean(type); 160 | if ((view.isDevelopmentMode() && developmentMode) || !view.isDevelopmentMode()) { 161 | menuEntries.add(new MenuEntry(view.getViewName(), view.getCaption(), view.getIcon(), view.getOrder(), view.getUserRole())); 162 | } 163 | } 164 | } 165 | AtomicInteger roleOrdinal = new AtomicInteger(getLoggedInUser().getRole() 166 | .ordinal()); 167 | menuEntries.stream() 168 | .sorted(Comparator.comparing(MenuEntry::getOrder)) 169 | .filter(m -> m.getRole() 170 | .ordinal() <= roleOrdinal.get()) 171 | .forEach(m -> initMenuEntry(m)); 172 | } 173 | 174 | private void initMenuEntry(MenuEntry menu) { 175 | Button button = new Button(menu.getCaption(), 176 | (ClickListener) event -> UI.getCurrent() 177 | .getNavigator() 178 | .navigateTo(menu.getName())); 179 | button.setPrimaryStyleName(ValoTheme.MENU_ITEM); 180 | button.setIcon(menu.getIcon()); 181 | menuItemsLayout.addComponent(button); 182 | menu.setButton(button); 183 | viewMenus.put(menu.getName(), menu); 184 | } 185 | 186 | /** 187 | * Highlights a view navigation button as the currently active view in the 188 | * menu. This method does not perform the actual navigation. 189 | * 190 | * @param viewName the name of the view to show as active 191 | */ 192 | public void setActiveView(String viewName) { 193 | for (MenuEntry menu : viewMenus.values()) { 194 | menu.getButton() 195 | .removeStyleName("selected"); 196 | } 197 | MenuEntry selected = viewMenus.get(viewName); 198 | if (selected != null) { 199 | selected.getButton() 200 | .addStyleName("selected"); 201 | } 202 | menuPart.removeStyleName(VALO_MENU_VISIBLE); 203 | } 204 | 205 | @Getter 206 | @RequiredArgsConstructor 207 | private static class MenuEntry { 208 | private final String name, caption; 209 | 210 | private final FontIcon icon; 211 | 212 | private final int order; 213 | 214 | private final UserRole role; 215 | 216 | @Setter 217 | private Button button; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/main/resources/io/rocketbase/toggl/ui/design.css: -------------------------------------------------------------------------------- 1 | .v-label.text-center { 2 | text-align: center; 3 | } 4 | 5 | .v-label.placeholder { 6 | opacity: 0.7; 7 | } 8 | 9 | .color-box { 10 | border-radius: 4px; 11 | width: 37px; 12 | height: 37px; 13 | border: 1px solid #c5c5c5; 14 | } 15 | 16 | .v-label.info { 17 | font-weight: bold; 18 | } 19 | 20 | .v-label.info.left-right { 21 | text-align: right; 22 | display: inline-block; 23 | position: relative; 24 | } 25 | 26 | .v-label.info span { 27 | font-weight: normal; 28 | font-style: italic; 29 | opacity: 0.8; 30 | } 31 | 32 | .v-label.info button { 33 | float: right; 34 | font-weight: 400; 35 | line-height: 1; 36 | } 37 | 38 | .v-label.info.left-right span { 39 | position: absolute; 40 | left: 0; 41 | } 42 | 43 | .v-widget.cell-content-wrapper { 44 | padding: 5px 2px; 45 | } 46 | 47 | .v-table-footer-container .footer-key-value { 48 | font-weight: bold; 49 | text-align: right; 50 | display: block; 51 | position: relative; 52 | width: 100%; 53 | margin-bottom: 5px; 54 | } 55 | 56 | .v-table-footer-container .footer-key-value:last-child { 57 | margin-bottom: 0; 58 | } 59 | 60 | .v-table-footer-container .footer-key-value span { 61 | font-weight: normal; 62 | font-style: italic; 63 | opacity: 0.8; 64 | position: absolute; 65 | left: 0; 66 | } 67 | 68 | .v-filterselect-item-color-palette span { 69 | padding-left: 23px; 70 | } 71 | 72 | .v-filterselect-item-color-palette:before { 73 | content: " "; 74 | width: 23px; 75 | height: 23px; 76 | border-radius: 4px; 77 | position: absolute; 78 | left: 1px; 79 | top: 1px; 80 | border: 1px solid #fff; 81 | } 82 | 83 | .v-filterselect-item-color-palette.color-palette-brick:before { 84 | background-color: #a54e3c; 85 | } 86 | 87 | .v-filterselect-item-color-palette.color-palette-pomegrante:before { 88 | background-color: #881f30; 89 | } 90 | 91 | .v-filterselect-item-color-palette.color-palette-cabernet:before { 92 | background-color: #721432; 93 | } 94 | 95 | .v-filterselect-item-color-palette.color-palette-aubergine:before { 96 | background-color: #5b1946; 97 | } 98 | 99 | .v-filterselect-item-color-palette.color-palette-indigo:before { 100 | background-color: #461c5d; 101 | } 102 | 103 | .v-filterselect-item-color-palette.color-palette-plum:before { 104 | background-color: #5c145e; 105 | } 106 | 107 | .v-filterselect-item-color-palette.color-palette-royal:before { 108 | background-color: #97348a; 109 | } 110 | 111 | .v-filterselect-item-color-palette.color-palette-french-blue:before { 112 | background-color: #3266ad; 113 | } 114 | 115 | .v-filterselect-item-color-palette.color-palette-teal:before { 116 | background-color: #337b8d; 117 | } 118 | 119 | .v-filterselect-item-color-palette.color-palette-aquamarine:before { 120 | background-color: #22586f; 121 | } 122 | 123 | .v-filterselect-item-color-palette.color-palette-marine:before { 124 | background-color: #18456b; 125 | } 126 | 127 | .v-filterselect-item-color-palette.color-palette-navy:before { 128 | background-color: #0e2e57; 129 | } 130 | 131 | .v-filterselect-item-color-palette.color-palette-lilac:before { 132 | background-color: #ae9cc7; 133 | } 134 | 135 | .v-filterselect-item-color-palette.color-palette-orchid:before { 136 | background-color: #985a9c; 137 | } 138 | 139 | .v-filterselect-item-color-palette.color-palette-cobalt:before { 140 | background-color: #4099d4; 141 | } 142 | 143 | .v-filterselect-item-color-palette.color-palette-pool:before { 144 | background-color: #9cd6f5; 145 | } 146 | 147 | .v-filterselect-item-color-palette.color-palette-periwinkle:before { 148 | background-color: #a7b4d0; 149 | } 150 | 151 | .v-filterselect-item-color-palette.color-palette-dove:before { 152 | background-color: #aec4d0; 153 | } 154 | 155 | .v-filterselect-item-color-palette.color-palette-duck-egg:before { 156 | background-color: #97babf; 157 | } 158 | 159 | .v-filterselect-item-color-palette.color-palette-turquoise:before { 160 | background-color: #6fbabf; 161 | } 162 | 163 | .v-filterselect-item-color-palette.color-palette-jade:before { 164 | background-color: #73aba0; 165 | } 166 | 167 | .v-filterselect-item-color-palette.color-palette-tiffany:before { 168 | background-color: #a5d2c1; 169 | } 170 | 171 | .v-filterselect-item-color-palette.color-palette-mint:before { 172 | background-color: #c8e0c4; 173 | } 174 | 175 | .v-filterselect-item-color-palette.color-palette-spring:before { 176 | background-color: #d3e2a3; 177 | } 178 | 179 | .v-filterselect-item-color-palette.color-palette-chive:before { 180 | background-color: #b8d26e; 181 | } 182 | 183 | .v-filterselect-item-color-palette.color-palette-tree-top:before { 184 | background-color: #759150; 185 | } 186 | 187 | .v-filterselect-item-color-palette.color-palette-bottle:before { 188 | background-color: #378159; 189 | } 190 | 191 | .v-filterselect-item-color-palette.color-palette-emerald:before { 192 | background-color: #29634e; 193 | } 194 | 195 | .v-filterselect-item-color-palette.color-palette-lemon:before { 196 | background-color: #fef6a6; 197 | } 198 | 199 | .v-filterselect-item-color-palette.color-palette-sunshine:before { 200 | background-color: #fcf151; 201 | } 202 | 203 | .v-filterselect-item-color-palette.color-palette-tumeric:before { 204 | background-color: #f9d549; 205 | } 206 | 207 | .v-filterselect-item-color-palette.color-palette-clementine:before { 208 | background-color: #f3b143; 209 | } 210 | 211 | .v-filterselect-item-color-palette.color-palette-peach:before { 212 | background-color: #f7d19d; 213 | } 214 | 215 | .v-filterselect-item-color-palette.color-palette-salmon:before { 216 | background-color: #f0b79f; 217 | } 218 | 219 | .v-filterselect-item-color-palette.color-palette-coral:before { 220 | background-color: #e89487; 221 | } 222 | 223 | .v-filterselect-item-color-palette.color-palette-watermelon:before { 224 | background-color: #e3708e; 225 | } 226 | 227 | .v-filterselect-item-color-palette.color-palette-rasberry:before { 228 | background-color: #ac2c69; 229 | } 230 | 231 | .v-filterselect-item-color-palette.color-palette-magenta:before { 232 | background-color: #db318a; 233 | } 234 | 235 | .v-filterselect-item-color-palette.color-palette-red-balloon:before { 236 | background-color: #db3656; 237 | } 238 | 239 | .v-filterselect-item-color-palette.color-palette-fire-engine:before { 240 | background-color: #db3732; 241 | } 242 | 243 | .v-filterselect-item-color-palette.color-palette-blood-orange:before { 244 | background-color: #e06151; 245 | } 246 | 247 | .v-filterselect-item-color-palette.color-palette-papaya:before { 248 | background-color: #e88b5b; 249 | } 250 | 251 | .v-filterselect-item-color-palette.color-palette-rose:before { 252 | background-color: #e6aab3; 253 | } 254 | 255 | .v-filterselect-item-color-palette.color-palette-cherry-blossom:before { 256 | background-color: #f6d6dc; 257 | } 258 | 259 | .v-filterselect-item-color-palette.color-palette-blush:before { 260 | background-color: #f6d6d1; 261 | } 262 | 263 | .v-filterselect-item-color-palette.color-palette-french-vanilla:before { 264 | background-color: #f8f1de; 265 | } 266 | 267 | .v-filterselect-item-color-palette.color-palette-ivory:before { 268 | background-color: #fbf8da; 269 | } 270 | 271 | .v-filterselect-item-color-palette.color-palette-clay:before { 272 | background-color: #e8d4b8; 273 | } 274 | 275 | .v-filterselect-item-color-palette.color-palette-sandstone:before { 276 | background-color: #c4b199; 277 | } 278 | 279 | .v-filterselect-item-color-palette.color-palette-sage:before { 280 | background-color: #a5b4a3; 281 | } 282 | 283 | .v-filterselect-item-color-palette.color-palette-moss:before { 284 | background-color: #a2a495; 285 | } 286 | 287 | .v-filterselect-item-color-palette.color-palette-olive:before { 288 | background-color: #7b7d6a; 289 | } 290 | 291 | .v-filterselect-item-color-palette.color-palette-smoke:before { 292 | background-color: #63605c; 293 | } 294 | 295 | .v-filterselect-item-color-palette.color-palette-espresso:before { 296 | background-color: #473937; 297 | } 298 | 299 | .v-filterselect-item-color-palette.color-palette-chocolate:before { 300 | background-color: #684940; 301 | } 302 | 303 | .v-filterselect-item-color-palette.color-palette-caramel:before { 304 | background-color: #957d62; 305 | } 306 | 307 | .v-filterselect-item-color-palette.color-palette-black:before { 308 | background-color: #221f20; 309 | } 310 | 311 | .v-filterselect-item-color-palette.color-palette-charcoal:before { 312 | background-color: #58585a; 313 | } 314 | 315 | .v-filterselect-item-color-palette.color-palette-slate:before { 316 | background-color: #949599; 317 | } 318 | 319 | .v-filterselect-item-color-palette.color-palette-silver-lining:before { 320 | background-color: #d1d2d4; 321 | } 322 | 323 | .v-filterselect-item-color-palette.color-palette-oyster:before { 324 | background-color: #e3ddd4; 325 | } 326 | 327 | .v-filterselect-item-color-palette.color-palette-stone:before { 328 | background-color: #d2cfc9; 329 | } 330 | 331 | .v-filterselect-item-color-palette.color-palette-smog:before { 332 | background-color: #928b88; 333 | } 334 | 335 | .v-filterselect-item-color-palette.color-palette-white:before { 336 | background-color: #ffffff; 337 | } -------------------------------------------------------------------------------- /src/main/java/io/rocketbase/toggl/ui/view/home/tab/WeekStatisticsTab.java: -------------------------------------------------------------------------------- 1 | package io.rocketbase.toggl.ui.view.home.tab; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.vaadin.shared.ui.ContentMode; 5 | import com.vaadin.spring.annotation.SpringComponent; 6 | import com.vaadin.spring.annotation.UIScope; 7 | import com.vaadin.ui.ComboBox; 8 | import com.vaadin.ui.Component; 9 | import com.vaadin.ui.Grid; 10 | import com.vaadin.ui.components.grid.FooterRow; 11 | import com.vaadin.ui.themes.ValoTheme; 12 | import io.rocketbase.toggl.backend.config.TogglService; 13 | import io.rocketbase.toggl.backend.model.ApplicationSetting.UserDetails; 14 | import io.rocketbase.toggl.backend.model.report.UserTimeline.WeekStatistics; 15 | import io.rocketbase.toggl.backend.model.report.WeekTimeline; 16 | import io.rocketbase.toggl.backend.service.TimeEntryService; 17 | import io.rocketbase.toggl.ui.component.tab.AbstractTab; 18 | import io.rocketbase.toggl.ui.view.home.HomeView; 19 | import org.joda.time.YearMonth; 20 | import org.threeten.extra.YearWeek; 21 | import org.vaadin.viritin.MSize; 22 | import org.vaadin.viritin.label.MLabel; 23 | import org.vaadin.viritin.layouts.MHorizontalLayout; 24 | import org.vaadin.viritin.layouts.MVerticalLayout; 25 | 26 | import javax.annotation.Resource; 27 | import java.util.List; 28 | 29 | 30 | @UIScope 31 | @SpringComponent 32 | public class WeekStatisticsTab extends AbstractTab { 33 | 34 | @Resource 35 | private TimeEntryService timeEntryService; 36 | 37 | @Resource 38 | private TogglService togglService; 39 | 40 | private MVerticalLayout layout; 41 | 42 | private ComboBox weekFrom = new ComboBox<>(), weekTo = new ComboBox<>(); 43 | 44 | 45 | @Override 46 | public Component initLayout() { 47 | layout = new MVerticalLayout() 48 | .withSize(MSize.FULL_SIZE) 49 | .withMargin(false) 50 | .add(HomeView.getPlaceHolder(), 1); 51 | 52 | 53 | weekFrom.setEmptySelectionAllowed(false); 54 | weekFrom.setTextInputAllowed(false); 55 | weekFrom.setWidth("100%"); 56 | weekFrom.addValueChangeListener(e -> { 57 | if (e.getValue() != null && weekTo.getValue() != null) { 58 | if (weekTo.getValue() 59 | .isBefore(e.getValue())) { 60 | weekTo.setValue(null); 61 | } 62 | } 63 | filter(); 64 | }); 65 | 66 | 67 | weekTo.setEmptySelectionAllowed(false); 68 | weekTo.setTextInputAllowed(false); 69 | weekTo.setWidth("100%"); 70 | weekTo.addValueChangeListener(e -> { 71 | if (e.getValue() != null) { 72 | if (weekFrom.getValue() != null && e.getValue() 73 | .isAfter(weekFrom.getValue())) { 74 | filter(); 75 | return; 76 | } else { 77 | weekTo.setValue(null); 78 | } 79 | } 80 | filter(); 81 | }); 82 | 83 | return new MVerticalLayout() 84 | .add(new MHorizontalLayout() 85 | .add(weekFrom, 1) 86 | .add(weekTo, 1) 87 | .withFullWidth()) 88 | .add(layout, 1) 89 | .withSize(MSize.FULL_SIZE); 90 | } 91 | 92 | @Override 93 | public void onTabEnter() { 94 | layout.removeAllComponents(); 95 | List yearWeekList = timeEntryService.fetchAllYearWeeks(); 96 | weekFrom.setItems(yearWeekList); 97 | weekTo.setItems(yearWeekList); 98 | filter(); 99 | } 100 | 101 | private void filter() { 102 | layout.removeAllComponents(); 103 | if (weekFrom.getValue() != null) { 104 | layout.add(genTable(timeEntryService.getWeekTimelines(weekFrom.getValue(), weekTo.getValue())), 1); 105 | } else { 106 | layout.add(HomeView.getPlaceHolder(), 1); 107 | } 108 | } 109 | 110 | 111 | protected Component genTable(List data) { 112 | 113 | Grid grid = new Grid<>(null, data); 114 | grid.addStyleName("week-statistics"); 115 | grid.setSizeFull(); 116 | grid.setBodyRowHeight(150); 117 | grid.setFooterRowHeight(75); 118 | 119 | Grid.Column weekCol = grid.addComponentColumn(e -> { 120 | return new MVerticalLayout() 121 | .withFullWidth() 122 | .withMargin(false) 123 | .withSpacing(false) 124 | .add(new MLabel(e.getYearWeek() 125 | .toString()).withStyleName(ValoTheme.LABEL_BOLD, ValoTheme.LABEL_LARGE)) 126 | .add(genLabelInfo("Total hours", e.getTotalHours())) 127 | .add(genLabelInfo("Billable hours", e.getBillableHours())) 128 | .add(genLabelInfo("Earned", e.getBillableAmount())) 129 | .add(genLabelInfo("Holidays", 130 | e.getHolidays() 131 | .size() > 0 ? "" : "-") 134 | .withStyleName("left-right")) 135 | .withStyleName("cell-content-wrapper"); 136 | }) 137 | .setCaption("week") 138 | .setWidth(240); 139 | 140 | String format = "
Hours: %s
Earned: %s
"; 141 | 142 | grid.setFooterVisible(true); 143 | FooterRow footer = grid.addFooterRowAt(0); 144 | footer.getCell(weekCol) 145 | .setHtml(String.format(format, 146 | Math.round(data.stream() 147 | .mapToDouble(e -> e.getTotalHours()) 148 | .sum() * 10.0) / 10.0, 149 | data.stream() 150 | .mapToLong(e -> e.getBillableAmount()) 151 | .sum())); 152 | 153 | 154 | List userList = togglService.getAllUsers(); 155 | userList.forEach(user -> { 156 | 157 | Grid.Column userCol = grid.addComponentColumn(e -> { 158 | if (e.getUidTimelines() 159 | .containsKey(user.getUid())) { 160 | WeekStatistics stat = e.getUidTimelines() 161 | .get(user.getUid()) 162 | .getWeekStatisticsOfWeek(e.getYearWeek()); 163 | return new MVerticalLayout() 164 | .withFullWidth() 165 | .withMargin(false) 166 | .withSpacing(false) 167 | .add(genLabelInfo("Total hours", stat.getTotalHours())) 168 | .add(genLabelInfo("Billable hours", stat.getBillableHours())) 169 | .add(genLabelInfo("Earned", stat.getBillableAmount())) 170 | .add(genLabelInfo("Days worked", stat.getWorkedDays())) 171 | .add(genLabelInfo("Ø per day", stat.getAverageHoursPerDay())) 172 | .withStyleName("cell-content-wrapper"); 173 | } else { 174 | return null; 175 | } 176 | }) 177 | .setCaption(user.getName()) 178 | .setWidth(190); 179 | 180 | footer.getCell(userCol) 181 | .setHtml(String.format(format, 182 | Math.round(data.stream() 183 | .filter(e -> e.getUidTimelines() 184 | .containsKey(user.getUid())) 185 | .mapToDouble(e -> e.getUidTimelines() 186 | .get(user.getUid()) 187 | .getWeekStatisticsOfWeek(e.getYearWeek()) 188 | .getTotalHours()) 189 | .sum() * 10.0) / 10.0, 190 | data.stream() 191 | .filter(e -> e.getUidTimelines() 192 | .containsKey(user.getUid())) 193 | .mapToLong(e -> e.getUidTimelines() 194 | .get(user.getUid()) 195 | .getWeekStatisticsOfWeek(e.getYearWeek()) 196 | .getBillableAmount()) 197 | .sum())); 198 | }); 199 | 200 | return grid; 201 | } 202 | 203 | private MLabel genLabelInfo(String caption, Number value) { 204 | return genLabelInfo(caption, String.valueOf(value)).withStyleName("left-right"); 205 | } 206 | 207 | private MLabel genLabelInfo(String caption, String value) { 208 | return new MLabel(String.format("%s: %s", caption, value)).withFullWidth() 209 | .withContentMode(ContentMode.HTML) 210 | .withStyleName("info"); 211 | } 212 | } 213 | --------------------------------------------------------------------------------