timekeeper

My first (abandoned unfinished) web application for time tracking
git clone git://jacobedwards.org/timekeeper
Log | Files | Refs | README

main.c (12491B)


      1 #include <assert.h>
      2 
      3 #define const
      4 
      5 #include <sys/types.h>
      6 #include <stdarg.h>
      7 #include <stdint.h>
      8 #include <time.h>
      9 #include <err.h>
     10 #include <kcgi.h>
     11 #include <stdio.h>
     12 #include <stdlib.h>
     13 #include <string.h>
     14 
     15 #include <sqlbox.h>
     16 
     17 #include "common.h"
     18 #include "pages.h"
     19 
     20 #include "../menu.h"
     21 #include "../times.h"
     22 #include "../util.h"
     23 
     24 #define DateTimeSize 25
     25 
     26 static char *datetime_fmt = "%FT%T+0000";
     27 
     28 enum kcgi_err
     29 printhours(struct pagedata *pd, struct timesheet *ts)
     30 {
     31 	int final;
     32 	enum kcgi_err status;
     33 
     34 	assert(ts->set & StartTimeFlag);
     35 
     36 	final = (ts->set & EndTimeFlag ||
     37 	    (ts->set & BreakStartTimeFlag && !(ts->set & BreakEndTimeFlag)));
     38 
     39 	status = htmlduration(pd, getduration(ts), final ? NULL : "counter", 1);
     40 	if (status != KCGI_OK)
     41 		return status;
     42 
     43 	if (!final) {
     44 		if ((status = khtml_elem(&pd->html, KELEM_NOSCRIPT)) != KCGI_OK ||
     45 		    (status = khtml_attr(&pd->html, KELEM_A,
     46 			KATTR_HREF, pd->pages[PageMain], KATTR__MAX)) != KCGI_OK ||
     47 		    (status = khtml_elem(&pd->html, KELEM_BUTTON)) != KCGI_OK ||
     48 		    (status = khtml_ncr(&pd->html, 0x21BB)) != KCGI_OK ||
     49 		    (status = khtml_closeelem(&pd->html, 3)) != KCGI_OK)
     50 			return status;
     51 	}
     52 
     53 	return KCGI_OK;
     54 }
     55 
     56 enum kcgi_err
     57 printtotal(struct pagedata *pd, struct timesheet *ts)
     58 {
     59 	enum kcgi_err status;
     60 	time_t t;
     61 	int i;
     62 
     63 	t = 0;
     64 	for (; ts; ts = ts->next)
     65 		if (ts->set & StartTimeFlag)
     66 			t += getduration(ts);
     67 
     68 	if ((status = khtml_attr(&pd->html,
     69 		KELEM_TH, KATTR_SCOPE, "row", KATTR__MAX)) != KCGI_OK ||
     70 	    (status = khtml_puts(&pd->html, "Total")) != KCGI_OK ||
     71 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
     72 		return status;
     73 
     74 	for (i = StartTime; i <= EndTime + 1; ++i)
     75 		if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK ||
     76 		    (i > EndTime && (status = htmlduration(pd, t, "total", 1)) != KCGI_OK) ||
     77 		    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
     78 			return status;
     79 	return KCGI_OK;
     80 }
     81 
     82 enum kcgi_err
     83 printtimebutton(struct pagedata *pd, enum time_field tf)
     84 {
     85 	enum kcgi_err status;
     86 	static char *buttons[] = {
     87 		[StartTime] = "Start",
     88 		[BreakStartTime] = "Start Break",
     89 		[BreakEndTime] = "End Break",
     90 		[EndTime] = "Stop Time"
     91 	};
     92 
     93 	if ((status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK ||
     94 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
     95 		KATTR_TYPE, "hidden",
     96 		KATTR_NAME, pd->keys[KeyTime].name,
     97 		KATTR_VALUE, timefields[tf],
     98 		KATTR__MAX)) != KCGI_OK ||
     99 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
    100 		KATTR_TYPE, "submit",
    101 		KATTR_VALUE, buttons[tf], KATTR__MAX)) != KCGI_OK ||
    102 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    103 		return status;
    104 	return KCGI_OK;
    105 }
    106 
    107 /* prints one row in times table */
    108 enum kcgi_err
    109 printtime(struct pagedata *pd, struct timesheet *ts)
    110 {
    111 	unsigned int i;
    112 	struct tm *tm;
    113 	char datetime[DateTimeSize];
    114 	char date[11]; /* %F */
    115 	char time[6]; /* %R */
    116 	enum kcgi_err status;
    117 
    118 	if (ts->set & StartTimeFlag) {
    119 		tm = gmtime(&ts->times[0]);
    120 		if (!tm)
    121 			return KCGI_SYSTEM;
    122 		if (strftime(date, sizeof(date), "%F", tm) >= sizeof(date))
    123 			return KCGI_SYSTEM;
    124 	} else {
    125 		date[0] = 0;
    126 	}
    127 
    128 	if ((status = khtml_attr(&pd->html, KELEM_TH,
    129 		KATTR_SCOPE, "row", KATTR__MAX)) != KCGI_OK ||
    130 	    (date[0] && (
    131 	    (status = khtml_elem(&pd->html, KELEM_TIME)) != KCGI_OK ||
    132 	    (status = khtml_puts(&pd->html, date)) != KCGI_OK ||
    133 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)) ||
    134 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    135 		return status;
    136 
    137 	for (i = 0; i < 4; ++i) {
    138 		if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK)
    139 			return status;
    140 		if (!(ts->set & timeflagmap[i])) {
    141 			/* This does not validate input */
    142 			if (i == StartTime)
    143 				status = printtimebutton(pd, StartTime);
    144 			else if (ts->set & StartTimeFlag &&
    145 			    i == BreakStartTime && !(ts->set & EndTimeFlag))
    146 				status = printtimebutton(pd, i);
    147 			else if (i == BreakEndTime && (ts->set & BreakStartTimeFlag))
    148 				status = printtimebutton(pd, i);
    149 			else if (ts->set & StartTimeFlag && i == EndTime)
    150 				status = printtimebutton(pd, i);
    151 			else
    152 				status = khtml_putc(&pd->html, '-');
    153 			if (status != KCGI_OK)
    154 				return status;
    155 		} else {
    156 			if (i) {
    157 				tm = gmtime(&ts->times[i]);
    158 				if (!tm)
    159 					return KCGI_SYSTEM;
    160 			}
    161 
    162 			if (strftime(datetime, sizeof(datetime), datetime_fmt, tm) >= sizeof(datetime) ||
    163 			    strftime(time, sizeof(time), "%R", tm) >= sizeof(time))
    164 				return KCGI_SYSTEM;
    165 
    166 			if ((status = khtml_attr(&pd->html, KELEM_TIME,
    167 				KATTR_DATETIME, datetime, KATTR__MAX)) != KCGI_OK ||
    168 			    (status = khtml_puts(&pd->html, time)) != KCGI_OK ||
    169 			    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    170 				return status;
    171 		}
    172 		if ((status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    173 			return status;
    174 	}
    175 
    176 	if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK ||
    177 	    (ts->set & StartTimeFlag && (status = printhours(pd, ts)) != KCGI_OK) ||
    178 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    179 		return status;
    180 	return KCGI_OK;
    181 }
    182 
    183 enum kcgi_err
    184 printtimefunc(struct pagedata *pd, struct timesheet *ts,
    185     char *key, char *name)
    186 {
    187 	enum kcgi_err status;
    188 
    189 	if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK ||
    190 	    (status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK ||
    191 	    (status = khtml_attrx(&pd->html, KELEM_INPUT,
    192 	        KATTR_TYPE, KATTRX_STRING, "hidden",
    193 	        KATTR_NAME, KATTRX_STRING, key,
    194 	        KATTR_VALUE, KATTRX_INT, (uint64_t)ts->entry,
    195 	        KATTR__MAX)) != KCGI_OK ||
    196 	    (status = khtml_attrx(&pd->html, KELEM_INPUT,
    197 	        KATTR_TYPE, KATTRX_STRING, "submit",
    198 	        KATTR_VALUE, KATTRX_STRING, name,
    199 	        KATTR__MAX)) != KCGI_OK ||
    200 	    (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK)
    201 		return status;
    202 	return KCGI_OK;
    203 }
    204 
    205 enum kcgi_err
    206 printtimefuncs(struct pagedata *pd, struct timesheet *ts)
    207 {
    208 	enum kcgi_err status;
    209 
    210 	if (ts->set & StartTimeFlag &&
    211 	    (status = printtimefunc(pd, ts, "delete", "Delete")) != KCGI_OK)
    212 		return status;
    213 	return KCGI_OK;
    214 }
    215 
    216 int
    217 iscurrent(struct pagedata *pd)
    218 {
    219 	return (pd->req.fieldmap[KeyPeriod] ?
    220 	    pd->req.fieldmap[KeyPeriod]->parsed.i : 0) == 0;
    221 }
    222 
    223 enum kcgi_err
    224 printtimes(struct pagedata *pd, struct timesheet *times)
    225 {
    226 	unsigned int i;
    227 	enum kcgi_err status;
    228 	char *headers[] = {
    229 		"Date", "Start time", "Break start", "Break end", "End time", "Hours", ""
    230 	};
    231 	struct timesheet *p;
    232 
    233 	if ((status = khtml_attr(&pd->html, KELEM_TABLE,
    234 	        KATTR_CLASS, "timetable", KATTR__MAX)) != KCGI_OK ||
    235 	    (status = htmlwithin(pd, KELEM_CAPTION, "Timesheets")) != KCGI_OK)
    236 		return status;
    237 
    238 	if ((status = khtml_elem(&pd->html, KELEM_THEAD)) != KCGI_OK ||
    239 	    (status = khtml_elem(&pd->html, KELEM_TR)) != KCGI_OK)
    240 		return status;
    241 
    242 	for (i = 0; i < Len(headers); ++i) {
    243 		if ((status = khtml_attr(&pd->html, KELEM_TH,
    244 			KATTR_SCOPE, "col", KATTR__MAX)) != KCGI_OK ||
    245 		    (status = khtml_puts(&pd->html, headers[i])) != KCGI_OK ||
    246 		    (i == 5 && (
    247 			(status = khtml_attr(&pd->html, KELEM_A,
    248 			    KATTR_HREF, "#total", KATTR__MAX)) != KCGI_OK ||
    249 			(status = khtml_ncr(&pd->html, 0x2193)) != KCGI_OK ||
    250 			(status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)) ||
    251 		    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    252 			return status;
    253 	}
    254 
    255 	if ((status = khtml_closeelem(&pd->html, 2)) != KCGI_OK ||
    256 	    (status = khtml_elem(&pd->html, KELEM_TBODY)) != KCGI_OK)
    257 		return status;
    258 
    259 	for (p = times; p->next; p = p->next)
    260 		;
    261 	for (; p; p = p->prev) {
    262 		if ((status = khtml_elem(&pd->html, KELEM_TR)) != KCGI_OK ||
    263 		    (status = printtime(pd, p)) != KCGI_OK ||
    264 		    (iscurrent(pd) && (status = printtimefuncs(pd, p)) != KCGI_OK) ||
    265 		    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    266 			return status;
    267 	}
    268 
    269 	if ((status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    270 		return status;
    271 
    272 	if (times->set & EndTimeFlag &&
    273 	   (times->next && times->next->set & StartTimeFlag)) {
    274 		if ((status = khtml_elem(&pd->html, KELEM_TFOOT)) != KCGI_OK ||
    275 		    (status = khtml_elem(&pd->html, KELEM_TR)) != KCGI_OK ||
    276 		    (status = printtotal(pd, times)) != KCGI_OK ||
    277 		    (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK)
    278 			return status;
    279 	}
    280 
    281 	return khtml_closeelem(&pd->html, 2);
    282 }
    283 
    284 int
    285 gettf(char *tfs)
    286 {
    287 	int i;
    288 
    289 	for (i = 0; i <= EndTime; ++i) {
    290 		if (strcmp(timefields[i], tfs) == 0)
    291 			return i;
    292 	}
    293 	return -1;
    294 }
    295 
    296 enum kcgi_err
    297 exportform(struct pagedata *pd)
    298 {
    299 	enum kcgi_err status;
    300 
    301 	if ((status = khtml_attr(&pd->html, KELEM_FORM,
    302 		KATTR_ACTION, pd->pages[PageExport], KATTR__MAX)) != KCGI_OK ||
    303 	    (status = khtml_elem(&pd->html, KELEM_LABEL)) != KCGI_OK ||
    304 	    ((pd->req.fieldmap[KeyPeriod] &&
    305 	        pd->req.fieldmap[KeyPeriod]->parsed.i > 0) &&
    306 	        (status = khtml_attrx(&pd->html, KELEM_INPUT,
    307 	        KATTR_TYPE, KATTRX_STRING, "hidden",
    308 	        KATTR_NAME, KATTRX_STRING, pd->keys[KeyPeriod].name,
    309 	        KATTR_VALUE, KATTRX_INT,
    310 	            (int64_t)pd->req.fieldmap[KeyPeriod]->parsed.i,
    311 	        KATTR__MAX)) != KCGI_OK) ||
    312 	    (status = khtml_putc(&pd->html, ' ')) != KCGI_OK ||
    313 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
    314 		KATTR_TYPE, "submit",
    315 		KATTR_VALUE, "Export", KATTR__MAX)) != KCGI_OK ||
    316 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
    317 		KATTR_TYPE, "checkbox",
    318 		KATTR_NAME, keys[KeyFormat].name,
    319 		KATTR_VALUE, ".epoch", KATTR__MAX)) != KCGI_OK ||
    320 	    (status = khtml_puts(&pd->html, "Epoch times")) != KCGI_OK ||
    321 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK ||
    322 	    (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK)
    323 		return status;
    324 	return KCGI_OK;
    325 }
    326 
    327 enum kcgi_err
    328 breakform(struct pagedata *pd)
    329 {
    330 	enum kcgi_err status;
    331 
    332 	if ((status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK ||
    333 	    (status = khtml_putc(&pd->html, ' ')) != KCGI_OK ||
    334 	    (status = khtml_elem(&pd->html, KELEM_LABEL)) != KCGI_OK ||
    335 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
    336 		KATTR_TYPE, "submit",
    337 		KATTR_VALUE, "End period", KATTR__MAX)) != KCGI_OK ||
    338 	    (status = khtml_attr(&pd->html, KELEM_INPUT,
    339 		KATTR_TYPE, "hidden",
    340 		KATTR_NAME, keys[KeyBreak].name,
    341 		KATTR_VALUE, "true", KATTR__MAX)) != KCGI_OK ||
    342 	    (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK)
    343 		return status;
    344 	return KCGI_OK;
    345 }
    346 
    347 enum kcgi_err
    348 pagemain(struct pagedata *pd)
    349 {
    350 	static char *css[] = {
    351 		"css/main.css", "css/timesheet.css", "css/menu.css", NULL
    352 	};
    353 	static char *scripts[] = {
    354 		"scripts/localize.js",
    355 		"scripts/counter.js",
    356 		NULL
    357 	};
    358 	static struct pagetemplate template = {
    359 		"Timesheet",
    360 		.css = css,
    361 		.scripts = scripts
    362 	};
    363 
    364 	enum kcgi_err status;
    365 	enum time_field tf;
    366 	struct timesheet *times, *end;
    367 	unsigned int period;
    368 	int current;
    369 	char heading[64];
    370 	int h1len;
    371 	struct menuitem menuitems[] = {
    372 		{ MenuFunc, { .func = breakform } },
    373 		{ MenuFunc, { .func = exportform } },
    374 		{ MenuURL, { .url = pd->pages[PageArchive] } }
    375 	};
    376 	int menuoffset;
    377 
    378 	if (!pd->user)
    379 		return tk_prompt_login(pd);
    380 
    381 	period = pd->req.fieldmap[KeyPeriod] ?
    382 	    pd->req.fieldmap[KeyPeriod]->parsed.i : 0;
    383 	current = period == 0;
    384 
    385 	if (current) {
    386 		if (pd->req.fieldmap[KeyBreak]) {
    387 			if (breaktime(pd, pd->user->hash))
    388 				err(1, "Unable to break time");
    389 		}
    390 		if (pd->req.fieldmap[KeyTime]) {
    391 			tf = gettf(pd->req.fieldmap[KeyTime]->parsed.s);
    392 			if (tf < 0)
    393 				err(1, "Invalid time field");
    394 			if (settime(pd, pd->user->hash, tf, time(NULL)))
    395 				return KCGI_SYSTEM;
    396 		}
    397 		if (pd->req.fieldmap[KeyDelete]) {
    398 			if (!kvalid_uint(pd->req.fieldmap[KeyDelete]))
    399 				return KCGI_SYSTEM;
    400 			if (deletetime(pd, pd->user->hash, pd->req.fieldmap[KeyDelete]->parsed.i))
    401 				return KCGI_SYSTEM;
    402 		}
    403 	}
    404 
    405 	status = tk_startpage(pd, &template, KHTTP_200);
    406 	if (status != KCGI_OK)
    407 		return status;
    408 
    409 	if (gettimes(pd, pd->user->hash, period, &times))
    410 		err(1, "gettimes");
    411 	if (current && !times && !(times = newtimesheet()))
    412 		err(1, "gettimes");
    413 
    414 	for (end = times; end->next; end = end->next)
    415 		;
    416 	if (current && end->set & EndTimeFlag &&
    417 	    !inserttimesheet(newtimesheet(), end)) {
    418 		freetimesheet(times);
    419 		return KCGI_SYSTEM;
    420 	}
    421 
    422 	if (current)
    423 		h1len = strlcpy(heading, "Timekeeper", sizeof(heading));
    424 	else
    425 		h1len = snprintf(heading, sizeof(heading), "Timekeeper, Period %d", period);
    426 	if (h1len < 0 || (unsigned int)h1len >= sizeof(heading))
    427 		return KCGI_SYSTEM;
    428 
    429 	menuoffset = current ? 0 : 1;
    430 
    431 	if ((status = htmlwithin(pd, KELEM_H1, heading)) != KCGI_OK ||
    432 	    (status = printtimes(pd, times)) != KCGI_OK ||
    433 	    (status = htmlmenu(pd, menuitems + menuoffset, Len(menuitems) - menuoffset)) != KCGI_OK) {
    434 		freetimesheet(times);
    435 		return status;
    436 	}
    437 	freetimesheet(times);
    438 
    439 	return endpage(pd, &template);
    440 }