diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed52319 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.timeclock/* \ No newline at end of file diff --git a/src/app/Invoice/InvoiceHandler.ts b/src/app/Invoice/InvoiceHandler.ts new file mode 100644 index 0000000..99d2623 --- /dev/null +++ b/src/app/Invoice/InvoiceHandler.ts @@ -0,0 +1,5 @@ +import { IInvoiceHandler } from "../../domain/Invoice/IInvoiceHandler.ts"; + +export class InvoiceHandler implements IInvoiceHandler { + +} \ No newline at end of file diff --git a/src/app/Punch/PunchHandler.ts b/src/app/Punch/PunchHandler.ts new file mode 100644 index 0000000..b4aa7c2 --- /dev/null +++ b/src/app/Punch/PunchHandler.ts @@ -0,0 +1,43 @@ +import { IPunchHandler } from "../../domain/Punch/IPunchHandler.ts"; +import { Punch } from "../../domain/Punch/Punch.ts"; +import { PunchType } from "../../domain/Punch/PunchType.ts"; +import { executeShellCommandAsync } from "../../infra/IO/Shell.ts"; +import * as FileManager from "../../infra/IO/Files.ts"; +import { Shift } from "../../domain/Shift/Shift.ts"; +import { ShiftHandler } from "../Shift/ShiftHandler.ts"; + +export class PunchHandler implements IPunchHandler { + constructor() { + } + + async createPunchAsync(punch: Punch): Promise { + if (punch.type === PunchType.Sart) { + await this.createStartPunchAsync(punch); + } else { + await this.createEndPunchAsync(punch); + } + await this.commitPunchAsync(punch); + } + + private async commitPunchAsync(punch: Punch) { + const msg = punch.type == PunchType.Sart ? "START" : "END"; + (await executeShellCommandAsync("git", ["commit", "-m", `\"TIMECLOCK PUNCH ${msg} - ${punch.user}\"`])).verifyZeroReturnCode(); + } + + private async createStartPunchAsync(punch: Punch) { + await FileManager.createDirectoryAsync(punch.punchDir); + await Deno.writeTextFile(punch.punchFilePath, Date.now().toString()); + (await executeShellCommandAsync("git", ["add", punch.punchFilePath])).verifyZeroReturnCode(); + } + + private async createEndPunchAsync(punch: Punch) { + const nowUtcMs = Date.now().toString(); + const punchStartUtcMs = await Deno.readTextFile(punch.punchFilePath); + const shift: Shift = new Shift(punch.user, punchStartUtcMs, nowUtcMs); + + await Deno.remove(punch.punchFilePath); + (await executeShellCommandAsync("git", ["add", punch.punchFilePath])).verifyZeroReturnCode(); + + await new ShiftHandler().createShiftAsync(shift); + } +} \ No newline at end of file diff --git a/src/app/Shift/ShiftHandler.ts b/src/app/Shift/ShiftHandler.ts new file mode 100644 index 0000000..06ad99b --- /dev/null +++ b/src/app/Shift/ShiftHandler.ts @@ -0,0 +1,13 @@ +import { IShiftHandler } from "../../domain/Shift/IShiftHandler.ts"; +import { Shift } from "../../domain/Shift/Shift.ts"; +import * as FileManager from "../../infra/IO/Files.ts"; +import { executeShellCommandAsync } from "../../infra/IO/Shell.ts"; + +export class ShiftHandler implements IShiftHandler { + + public async createShiftAsync(shift: Shift): Promise { + await FileManager.createDirectoryAsync(shift.shiftDir); + await Deno.writeTextFile(shift.shiftFilePath, shift.diffHours.toString()); + (await executeShellCommandAsync("git", ["add", shift.shiftFilePath])).verifyZeroReturnCode(); + } +} \ No newline at end of file diff --git a/src/app/timeclock.ts b/src/app/timeclock.ts new file mode 100644 index 0000000..b795836 --- /dev/null +++ b/src/app/timeclock.ts @@ -0,0 +1,24 @@ +import { Punch } from "../domain/Punch/Punch.ts"; +import { PunchType } from "../domain/Punch/PunchType.ts"; +import { executeShellCommandAsync } from "../infra/IO/Shell.ts"; +import { PunchHandler } from "./Punch/PunchHandler.ts"; + +async function validateCleanWorkingTreeAsync() { + const statusOutput = (await executeShellCommandAsync("git", ["status"])).verifyZeroReturnCode(); + if (!statusOutput.stdout.includes("nothing to commit, working tree clean")) { + throw "punches must occur on a clean working tree"; + } +} +const _punchHandler = new PunchHandler(); +async function main() { + const user: string = Deno.args[0]; + const isEndPunch: boolean = Deno.args.some(x => x === "--end"); + const punchType: PunchType = isEndPunch ? PunchType.End : PunchType.Sart; + + const punch = new Punch(punchType, user); + + await validateCleanWorkingTreeAsync(); + await _punchHandler.createPunchAsync(punch); +} + +await main(); diff --git a/src/domain/Constants.ts b/src/domain/Constants.ts new file mode 100644 index 0000000..3551177 --- /dev/null +++ b/src/domain/Constants.ts @@ -0,0 +1 @@ +export const ONE_HOUR_IN_MS: number = 3600000; \ No newline at end of file diff --git a/src/domain/Invoice/IInvoiceHandler.ts b/src/domain/Invoice/IInvoiceHandler.ts new file mode 100644 index 0000000..ce4f83c --- /dev/null +++ b/src/domain/Invoice/IInvoiceHandler.ts @@ -0,0 +1,3 @@ +export interface IInvoiceHandler { + +} \ No newline at end of file diff --git a/src/domain/Invoice/Invoice.ts b/src/domain/Invoice/Invoice.ts new file mode 100644 index 0000000..ea8b604 --- /dev/null +++ b/src/domain/Invoice/Invoice.ts @@ -0,0 +1,3 @@ +export class Invoice { + +} \ No newline at end of file diff --git a/src/domain/Punch/IPunchHandler.ts b/src/domain/Punch/IPunchHandler.ts new file mode 100644 index 0000000..cdc95d4 --- /dev/null +++ b/src/domain/Punch/IPunchHandler.ts @@ -0,0 +1,5 @@ +import { Punch } from "./Punch.ts"; + +export interface IPunchHandler { + createPunchAsync(punch: Punch): Promise; +} \ No newline at end of file diff --git a/src/domain/Punch/Punch.ts b/src/domain/Punch/Punch.ts new file mode 100644 index 0000000..4b8e105 --- /dev/null +++ b/src/domain/Punch/Punch.ts @@ -0,0 +1,15 @@ +import { PunchType } from "./PunchType.ts"; + +export class Punch { + type: PunchType; + user: string; + punchDir: string; + punchFilePath: string; + + constructor(punchType: PunchType, user: string) { + this.type = punchType; + this.user = user; + this.punchDir = `./.timeclock/punches/${user}`; + this.punchFilePath = `${this.punchDir}/punch`; + } +} \ No newline at end of file diff --git a/src/domain/Punch/PunchType.ts b/src/domain/Punch/PunchType.ts new file mode 100644 index 0000000..4a1df3f --- /dev/null +++ b/src/domain/Punch/PunchType.ts @@ -0,0 +1,4 @@ +export enum PunchType { + Sart, + End +} \ No newline at end of file diff --git a/src/domain/Shift/IShiftHandler.ts b/src/domain/Shift/IShiftHandler.ts new file mode 100644 index 0000000..024bf11 --- /dev/null +++ b/src/domain/Shift/IShiftHandler.ts @@ -0,0 +1,5 @@ +import { Shift } from "./Shift.ts"; + +export interface IShiftHandler { + createShiftAsync(shift: Shift): Promise; +} \ No newline at end of file diff --git a/src/domain/Shift/Shift.ts b/src/domain/Shift/Shift.ts new file mode 100644 index 0000000..3622038 --- /dev/null +++ b/src/domain/Shift/Shift.ts @@ -0,0 +1,16 @@ +import * as DomainConstants from "../Constants.ts"; + +export class Shift { + user: string; + diffHours: number + shiftDir: string; + shiftFilePath: string; + + constructor(user: string, punchStartUtcMs: string, punchEndUtcMs: string) { + this.user = user; + const diffMs: number = Number.parseInt(punchEndUtcMs) - Number.parseInt(punchStartUtcMs); + this.diffHours = diffMs/DomainConstants.ONE_HOUR_IN_MS; + this.shiftDir = `./.timeclock/shifts/${user}`; + this.shiftFilePath = `${this.shiftDir}/shift_${crypto.randomUUID()}`; + } +} \ No newline at end of file diff --git a/src/infra/IO/Files.ts b/src/infra/IO/Files.ts new file mode 100644 index 0000000..ef1f029 --- /dev/null +++ b/src/infra/IO/Files.ts @@ -0,0 +1,10 @@ +import {exists} from "https://deno.land/std@0.201.0/fs/mod.ts"; + +export async function createDirectoryAsync(path: string) { + const dirExists = await exists(path); + if (!dirExists) { + await Deno.mkdir(path, { + recursive: true + }); + } +} \ No newline at end of file diff --git a/src/infra/IO/Shell.ts b/src/infra/IO/Shell.ts new file mode 100644 index 0000000..61825bb --- /dev/null +++ b/src/infra/IO/Shell.ts @@ -0,0 +1,6 @@ +import { ShellOutput } from "./ShellOutput.ts"; + +export async function executeShellCommandAsync(command: string, args: string[]): Promise { + const output = await new Deno.Command(command, { args: args }).output(); + return new ShellOutput(output); +} \ No newline at end of file diff --git a/src/infra/IO/ShellOutput.ts b/src/infra/IO/ShellOutput.ts new file mode 100644 index 0000000..2db547c --- /dev/null +++ b/src/infra/IO/ShellOutput.ts @@ -0,0 +1,28 @@ +export class ShellOutput { + private _output: Deno.CommandOutput; + + constructor(output: Deno.CommandOutput) { + this._output = output; + } + + public get code(): number { + return this._output.code; + } + + public get stderr(): string { + return new TextDecoder().decode(this._output.stderr); + } + + public get stdout(): string { + return new TextDecoder().decode(this._output.stdout); + } + + public verifyZeroReturnCode(): ShellOutput { + if (this._output.code !== 0) { + // todo - enhance error handling + console.log(this.stdout, this.stderr); + throw `non-zero RC: ${this._output.code}`; + } + return this; + } +} \ No newline at end of file diff --git a/src/timeclock.ts b/src/timeclock.ts deleted file mode 100644 index d6b70d2..0000000 --- a/src/timeclock.ts +++ /dev/null @@ -1,75 +0,0 @@ -enum PunchType { - Sart, - End -} - -const ONE_HOUR_IN_MS = 3600000; - -import {exists} from "https://deno.land/std@0.201.0/fs/mod.ts"; - -const user: string = Deno.args[0]; -const isEndPunch: boolean = Deno.args.some(x => x === "--end"); -const punchType: PunchType = isEndPunch ? PunchType.End : PunchType.Sart; -const punchDir = `./.timeclock/punches/${user}`; -const punchFilePath = `${punchDir}/punch`; -const shiftDir = `./.timeclock/shifts/${user}`; -const shiftFilePath = `${shiftDir}/shift_${crypto.randomUUID()}`; - -async function executeShellCommand(command: string, args: string[]): Promise { - const cmd = new Deno.Command(command, { args: args }); - const { code, stdout, stderr } = await cmd.output(); - if (code !== 0) { - console.log(new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)); - throw `non-zero RC: ${code}`; - } - return new TextDecoder().decode(stdout); -} - -async function createDirectory(path: string) { - const dirExists = await exists(path); - if (!dirExists) { - await Deno.mkdir(path, { - recursive: true - }); - } -} - -async function createPunch() { - if (punchType === PunchType.Sart) { - await createStartPunch(); - } else { - await createEndPunch(); - } -} - -async function createStartPunch() { - await Deno.writeTextFile(punchFilePath, Date.now().toString()); - await executeShellCommand("git", ["add", punchFilePath]); - await executeShellCommand("git", ["commit", "-m", `\"TIMECLOCK PUNCH START - ${user}\"`]); -} - -async function createEndPunch() { - await createDirectory(shiftDir); - const nowUtcMs = Date.now().toString(); - const punchStartUtcMs = await Deno.readTextFile(punchFilePath); - await Deno.remove(punchFilePath); - const diffMs: number = Number.parseInt(nowUtcMs) - Number.parseInt(punchStartUtcMs); - const diffHors = diffMs/ONE_HOUR_IN_MS; - await Deno.writeTextFile(shiftFilePath, diffHors.toString()); - await executeShellCommand("git", ["add", punchFilePath]); - await executeShellCommand("git", ["add", shiftFilePath]); - await executeShellCommand("git", ["commit", "-m", `\"TIMECLOCK PUNCH END - ${user}\"`]); -} - -async function main() { - const statusOutput = await executeShellCommand("git", ["status"]); - if (!statusOutput.includes("nothing to commit, working tree clean")) { - throw "punches must occur on a clean working tree"; - } - else { - await createDirectory(punchDir); - await createPunch(); - } -} - -await main();