Time tracking you can host anywhere, built on Django. Full export support in multiple formats and easily extensible.
djangodockerhandcodedpythonself-hostedtime-trackingtimetracker
1<template>
2 <div class="bg-light py-2">
3 <form id="tracker" class="container-fluid" @submit.prevent="submitEntry">
4 <div class="row">
5 <div class="col-sm-3 px-2" @keyup.enter.prevent>
6 <selector
7 id="tracker-client"
8 v-model="client"
9 :options="clients"
10 :selected="client"
11 placeholder="Client"
12 />
13 </div>
14
15 <div class="col-sm-3 px-2" @keyup.enter.prevent>
16 <selector
17 id="tracker-project"
18 v-model="project"
19 :options="projects"
20 :selected="project"
21 placeholder="Project"
22 required
23 />
24 </div>
25
26 <div class="col-sm-3 px-2" @keyup.enter.prevent>
27 <selector
28 id="tracker-task"
29 v-model="task"
30 :options="tasks"
31 :selected="task"
32 placeholder="Task"
33 required
34 />
35 </div>
36
37 <div class="col-sm-3 px-2">
38 <datepicker
39 v-model="date"
40 id="tracker-date"
41 type="text"
42 class="form-control date-input text-center"
43 placeholder="Date"
44 />
45 </div>
46 </div>
47
48 <div class="row mt-2">
49 <div class="col-sm-6 px-2">
50 <input
51 id="tracker-note"
52 v-model="note"
53 class="form-control w-100 shadow-sm"
54 placeholder="Note"
55 type="text"
56 />
57 </div>
58
59 <div class="col-sm-3 px-2">
60 <input
61 id="tracker-duration"
62 v-model="duration"
63 class="form-control text-right font-weight-bold w-100 shadow-sm"
64 placeholder="0:00"
65 type="text"
66 required
67 />
68 </div>
69
70 <div class="col-sm-3 px-2">
71 <button
72 v-if="!running && !duration"
73 class="btn btn-success w-100"
74 @click.prevent.exact="toggle"
75 >
76 <icon :icon="['fas', 'play']" class="mr-1" />
77 Start
78 </button>
79 <button
80 v-else-if="running"
81 class="btn btn-danger w-100"
82 @click.prevent.exact="toggle"
83 >
84 <icon :icon="['fas', 'stop']" class="mr-1" />
85 Stop ({{ seconds }})
86 </button>
87 <button
88 v-else-if="!running && duration"
89 :disabled="!canSubmit"
90 id="tracker-submit"
91 class="btn btn-info w-100"
92 type="submit"
93 >
94 <icon :icon="['fas', 'plus']" class="mr-1" />
95 Add
96 </button>
97 </div>
98 </div>
99 </form>
100 </div>
101</template>
102
103<script>
104import { mapGetters, mapActions } from "vuex";
105
106import Selector from "./selector.vue";
107import Datepicker from "./datepicker.vue";
108
109function getFromLocalStorage(key, defaultValue) {
110 if (!key in window.localStorage) {
111 return defaultValue;
112 }
113 if (defaultValue === null && window.localStorage[key] === "null") {
114 return null;
115 }
116 if (defaultValue === false) {
117 return window.localStorage[key] === "true";
118 }
119 return window.localStorage[key];
120}
121
122export default {
123 components: {
124 Selector,
125 Datepicker
126 },
127 data() {
128 return {
129 task: getFromLocalStorage("tracker_task", null),
130 client: null,
131 project: getFromLocalStorage("tracker_project", null),
132 note: getFromLocalStorage("tracker_note", null),
133 duration: null,
134 datetimeStart:
135 "tracker_start" in window.localStorage
136 ? new Date(Number(window.localStorage.tracker_start))
137 : null,
138 datetimeEnd: null,
139 date: this.$moment().format("YYYY-MM-DD"),
140
141 running: getFromLocalStorage("tracker_running", false),
142 total: 0,
143 seconds: "0"
144 };
145 },
146 computed: {
147 ...mapGetters({
148 clients: "clients/getSelectClients",
149 tasks: "tasks/getSelectTasks",
150 projectsAll: "projects/getSelectProjects",
151 projectsClient: "projects/getSelectProjectsForClient",
152 users: "users/getSelectUsers",
153 getProject: "projects/getProject"
154 }),
155 projects() {
156 if (!this.client) {
157 return this.projectsAll;
158 } else {
159 return this.projectsClient(this.client);
160 }
161 },
162 canSubmit() {
163 return this.project && this.task;
164 }
165 },
166 watch: {
167 client() {
168 if (this.project) {
169 const p = this.getProject();
170 if (p.client !== this.client) {
171 this.project = null;
172 }
173 }
174 },
175 project() {
176 window.localStorage.tracker_project = this.project;
177 },
178 task() {
179 window.localStorage.tracker_task = this.task;
180 },
181 note() {
182 window.localStorage.tracker_note = this.note;
183 }
184 },
185 created() {
186 if (this.running) {
187 this.tick();
188 this.start();
189 }
190 },
191 methods: {
192 ...mapActions({
193 createEntry: "entries/createEntry"
194 }),
195 submitEntry() {
196 this.createEntry({
197 task: this.task,
198 project: this.project,
199 note: this.note,
200 duration: this.duration,
201 datetime_start: this.datetimeStart,
202 datetime_end: this.datetimeEnd,
203 user: timestrapConfig.USER.URL,
204 date: this.date //this.$moment().format('YYYY-MM-DD'),
205 });
206 this.reset();
207 this.note = null;
208 },
209 reset() {
210 this.duration = null;
211 this.total = 0;
212 this.seconds = "0";
213 },
214 tick() {
215 this.total = Math.floor(
216 (Date.now() - this.datetimeStart.valueOf()) / 1000
217 );
218 this.seconds = (this.total % 3600) % 60;
219 this.duration = this.$moment
220 .duration(this.total, "seconds")
221 .format("h:mm", { trim: false });
222 document.title = this.duration + " — " + timestrapConfig.SITE.NAME;
223 },
224 start() {
225 this.interval = setInterval(this.tick, 1000, this);
226 },
227 toggle() {
228 this.running = !this.running;
229 window.localStorage.tracker_running = this.running;
230
231 if (this.running) {
232 this.datetimeStart = new Date(Date.now());
233 window.localStorage.tracker_start = this.datetimeStart.getTime();
234 this.reset();
235 this.start();
236 } else {
237 this.datetimeEnd = new Date(Date.now());
238 window.localStorage.tracker_start = null;
239 clearInterval(this.interval);
240 if (this.total < 60) this.reset();
241 document.title = "Application — " + timestrapConfig.SITE.NAME;
242 }
243 }
244 }
245};
246</script>