From fb36fb8303ba9d1e862be67c396212f3eaa89c55 Mon Sep 17 00:00:00 2001 From: Tim Bendt Date: Fri, 1 Nov 2024 11:32:37 -0500 Subject: [PATCH] wip --- src/components/detailsInvoice.ts | 82 ++++++++++++ src/components/helpers/animatedText.ts | 42 ++++++ src/components/helpers/boxBorders.ts | 5 + src/components/helpers/dates.ts | 21 +++ src/components/helpers/tableCellBox.ts | 53 ++++++++ src/components/listClientInvoices.ts | 71 ----------- src/components/listClients.ts | 104 +++++++++++++++ src/components/listInvoices.ts | 112 ++++++++++++++++ src/components/mainNav.ts | 38 ++++++ src/index.ts | 67 +++++----- src/services/invoiceResponse.ts | 146 +++++++++++++++++++++ src/services/pancakeApi.ts | 169 ++++++++++++++++--------- 12 files changed, 751 insertions(+), 159 deletions(-) create mode 100644 src/components/detailsInvoice.ts create mode 100644 src/components/helpers/animatedText.ts create mode 100644 src/components/helpers/boxBorders.ts create mode 100644 src/components/helpers/dates.ts create mode 100644 src/components/helpers/tableCellBox.ts delete mode 100644 src/components/listClientInvoices.ts create mode 100644 src/components/listClients.ts create mode 100644 src/components/listInvoices.ts create mode 100644 src/components/mainNav.ts create mode 100644 src/services/invoiceResponse.ts diff --git a/src/components/detailsInvoice.ts b/src/components/detailsInvoice.ts new file mode 100644 index 0000000..43d4e74 --- /dev/null +++ b/src/components/detailsInvoice.ts @@ -0,0 +1,82 @@ +import * as w from "wretched"; +import Table from "cli-table3"; +import { + getOneInvoice, + markInvoicePaid, + updateInvoice, +} from "../services/pancakeApi.js"; +import { doubleBoxWithTitle } from "./helpers/boxBorders.js"; +import { isNaN, omit, values } from "lodash-es"; +import { fromUnixTime, format } from "date-fns"; +import { InvoiceDetailsResponse } from "../services/invoiceResponse.js"; + +export const renderInvoiceDetails = async (invoiceId: string | number) => { + const { invoice } = await getOneInvoice(invoiceId.toString()); + if (process.env.DEBUG) { + console.log("🚀 ~ renderInvoiceDetails ~ details:", invoice); + } + const mainWindow = new w.Scrollable({ + children: [], + }); + const mainTable = new Table(); + for (const key in omit(invoice, "items", "partial_payments")) { + const rawValue = invoice[key as keyof InvoiceDetailsResponse]; + if (typeof rawValue !== "object") { + let value: Table.Cell = rawValue; + if (typeof rawValue !== "boolean") { + if (key.indexOf("date") >= 0) { + value = Number(rawValue); + if (!isNaN(value)) { + value = format(fromUnixTime(value), "y-M-d"); + } + } + } + mainTable.push({ + [key]: value, + } as Table.VerticalTableRow); + } + } + mainWindow.addAll([ + new w.Flow({ + direction: "topToBottom", + children: [ + new w.Text({ text: `Unpaid: $${invoice.unpaid_amount}` }), + new w.Button({ + text: `Mark as ${invoice.unpaid_amount > 0 ? "Paid" : "Unpaid"}`, + onClick: async () => { + const isPaid = invoice.unpaid_amount > 0; + if (process.env.DEBUG) { + console.log("Switching paid status from", { + isPaid, + invoiceId, + }); + } + if (isPaid) { + mainWindow.removeAllChildren(); + mainWindow.addAll([ + new w.Flow({ + direction: "leftToRight", + children: [ + new w.Text({ text: "Paid Amount" }), + new w.Input({ text: "Paid Amount" }), + ], + }), + ]); + } else { + if (!process.env.DEBUG) { + await markInvoicePaid(invoiceId); + } + } + }, + }), + new w.Text({ text: mainTable.toString() }), + ], + }), + ]); + return new w.Box({ + border: doubleBoxWithTitle( + `${invoice.client_name} 💸 Invoice: ${invoice.id}`, + ), + child: mainWindow, + }); +}; diff --git a/src/components/helpers/animatedText.ts b/src/components/helpers/animatedText.ts new file mode 100644 index 0000000..49abfe5 --- /dev/null +++ b/src/components/helpers/animatedText.ts @@ -0,0 +1,42 @@ +import { Size, Text, View, Viewport } from "wretched"; + +export class AnimatedText extends View { + #frameTime = 0; + #frame = 0; + #frames: string[]; + + static FRAME = 32; + + constructor({ frames }: { frames: string[] }) { + super({ + x: 10, + y: 4, + width: 14, + height: 2, + }); + + this.#frames = frames; + } + + naturalSize() { + return new Size(14, 2); + } + + receiveTick(dt: number): boolean { + this.#frameTime += dt; + if (this.#frameTime > AnimatedText.FRAME) { + this.#frameTime %= AnimatedText.FRAME; + this.#frame = (this.#frame + 1) % this.#frames.length; + return true; + } + + return false; + } + + render(viewport: Viewport) { + viewport.registerTick(); + + const t = new Text({ text: this.#frames[this.#frame] }); + t.render(viewport); + } +} diff --git a/src/components/helpers/boxBorders.ts b/src/components/helpers/boxBorders.ts new file mode 100644 index 0000000..da38020 --- /dev/null +++ b/src/components/helpers/boxBorders.ts @@ -0,0 +1,5 @@ +export const doubleBoxWithTitle = ( + title: string, +): [string, string, string, string, string, string, string, string] => { + return ["═", "║", `╔═ ${title} `, "╗", "╚", "╝", "═", "║"]; +}; diff --git a/src/components/helpers/dates.ts b/src/components/helpers/dates.ts new file mode 100644 index 0000000..9ee4ab6 --- /dev/null +++ b/src/components/helpers/dates.ts @@ -0,0 +1,21 @@ +import { differenceInBusinessDays, format, parse, startOfDay } from "date-fns"; + +const now = format(new Date(), "yyyy-MM-dd"); +const customTime = "18:00:00"; +const customDate = new Date(`${now} ${customTime}`); +export const simpleFormat = (date: Date) => { + return format(startOfDay(date), "MMM d, yyyy"); +}; + +console.log("🚀 ~ customDate:", simpleFormat(customDate)); + +export const distanceInBizDays = (date: Date) => + differenceInBusinessDays(date, customDate); + +/** + * + * @param dateString Ex. 2017-07-16 23:00:00 + * @returns Date + */ +export const parseServiceDateString = (dateString: string) => + parse(dateString, "yyyy-MM-dd hh:mm:ss", customDate); diff --git a/src/components/helpers/tableCellBox.ts b/src/components/helpers/tableCellBox.ts new file mode 100644 index 0000000..2bb8553 --- /dev/null +++ b/src/components/helpers/tableCellBox.ts @@ -0,0 +1,53 @@ +import { Box, Text } from "wretched"; +import { InvoiceResponse } from "../../services/pancakeApi.js"; +import { Theme } from "wretched/dist/Theme.js"; +import { Style } from "wretched/dist/Style.js"; + +export type CellProps = { + width?: number; + padding?: { left: number; right: number; top: number; bottom: number }; + borders?: { left?: boolean; right?: boolean }; + alignment?: "left" | "right" | "center"; + theme?: Theme; +}; +export const tableCellBox = (text: string, opts: CellProps) => { + opts = Object.assign( + { + padding: { left: 1, right: 1, top: 0, bottom: 0 }, + borders: { left: true, right: true }, + alignment: "left", + theme: Theme.plain, + }, + opts, + ); + const innerWidth = opts.width + ? opts.padding + ? opts.width + opts.padding.left + opts.padding.right + : "natural" + : "natural"; + return new Box({ + width: innerWidth, + padding: opts.padding, + theme: opts.theme, + child: new Text({ + wrap: false, + style: new Style({ + background: opts.theme?.background, + foreground: opts.theme?.textColor, + }), + text, + width: opts.width, + alignment: opts.alignment, + }), + border: [ + "", + opts.borders?.left ? "|" : "", + "", + "", + "", + "", + "", + opts.borders?.right ? "|" : "", + ], + }); +}; diff --git a/src/components/listClientInvoices.ts b/src/components/listClientInvoices.ts deleted file mode 100644 index 99c66aa..0000000 --- a/src/components/listClientInvoices.ts +++ /dev/null @@ -1,71 +0,0 @@ - -import { Box, Button, Container, Flex, Screen, ScrollableList, Text, } from "wretched"; -import { ClientResponse, InvoiceResponse, getAllClients, getAllInvoices, getAllProjects } from "../services/pancakeApi.js"; -import { compact, find, reverse, sortBy } from "lodash-es"; - -const clientsResp = await getAllClients({ limit: 100, sort_by: "created", sort_dir: "desc" }); -console.log("🚀 ~ clientsResp:", clientsResp) -const sorted = reverse(sortBy(clientsResp.clients, ["unpaid_total"])) -console.log("🚀 ~ sorted:", sorted) - -let selectedClientId: string | undefined; -const currentClientName = () => { - const curr = find(sorted, { id: selectedClientId }); - return `🏙️ ${curr?.company} | ${curr?.first_name} ${curr?.last_name}` -} -let invoiceList = async (clientId?: string) => { - if (clientId === undefined) { - console.warn("No Client Id"); - return new Text({ text: " --" }); - } - const invResp = await getAllInvoices({ client_id: clientId }); - const invoices: Array = invResp.invoices; - if (!invoices?.length) { - console.warn("No invoices") - return new Text({ text: " --" }); - } - return new ScrollableList({ cellForItem: (item) => new Button({ text: item.invoice_number }), items: invoices }) -} - -let Invoices = new Box({ width: "fill", height: "fill", child: await invoiceList(selectedClientId), border: "rounded" }) - -let Clients = new ScrollableList({ - minWidth: 20, - width: "natural", - cellForItem: (item: ClientResponse) => { - if (!item || !item.company) { - console.log(item) - return new Text({ text: "Nothing Here" }) - } - return new Button({ - text: item.company || item.id, onClick: async () => { - console.log("🚀 ~ clientInvoicesView ~ clicked company:", item); - selectedClientId = item.id; - //TODO: Need a spinner for loading indicator - Invoices.removeAllChildren(); - Invoices.add(await invoiceList(selectedClientId)) - ClientTitle.text = currentClientName(); - }, - }); - }, items: sorted -}) - -const ClientTitle = new Text({ - text: "No Client Selected Yet", -}) - -export const clientInvoicesView = Flex.right({ - children: [ - Clients, - Flex.down({ - width: "fill", - height: "fill", - padding: 1, - - children: [ - ClientTitle, - Invoices - ], - }), - ] -}) diff --git a/src/components/listClients.ts b/src/components/listClients.ts new file mode 100644 index 0000000..9c31dfd --- /dev/null +++ b/src/components/listClients.ts @@ -0,0 +1,104 @@ +import { + Box, + Button, + Container, + Flex, + Screen, + ScrollableList, + Text, +} from "wretched"; +import { + ClientResponse, + InvoiceResponse, + getAllClients, + getAllInvoices, + getAllProjects, +} from "../services/pancakeApi.js"; +import spinners from "cli-spinners"; +import { compact, find, isNil, isString, reverse, sortBy } from "lodash-es"; +import { renderInvoiceList } from "./listInvoices.js"; +import { renderInvoiceDetails } from "./detailsInvoice.js"; +import { doubleBoxWithTitle } from "./helpers/boxBorders.js"; +import { AnimatedText } from "./helpers/animatedText.js"; + +const clientsResp = await getAllClients({ + limit: 100, + sort_by: "created", + sort_dir: "desc", +}); +// console.log("🚀 ~ clientsResp:", clientsResp); +const sorted = reverse(sortBy(clientsResp.clients, ["unpaid_total"])); +// console.log("🚀 ~ sorted:", sorted); + +// let selectedClientId: string | undefined; +// let selectedInvoiceId: string | undefined; + +const reDrawMain = async ( + selectedClientId?: string, + selectedInvoiceId?: string, +) => { + clientInvoicesView.removeChild(1); + clientInvoicesView.add( + new AnimatedText({ frames: spinners.aesthetic.frames }), + ); + const newBox = await renderInvoicesBox(selectedClientId, selectedInvoiceId); + clientInvoicesView.removeChild(1); + clientInvoicesView.add(renderRightPanel(newBox)); +}; + +const renderInvoicesBox = async ( + selectedClientId?: string, + selectedInvoiceId?: string, +) => { + let InvoicesBox = new Box({ child: new Text({ text: "« Select a Client" }) }); + + if (selectedInvoiceId && selectedClientId) { + InvoicesBox = await renderInvoiceDetails(selectedInvoiceId); + } else if (selectedClientId) { + const currClient = find(sorted, { id: selectedClientId }); + const currentClientName = `${currClient?.company} | ${currClient?.first_name} ${currClient?.last_name}`; + InvoicesBox = new Box({ + width: "fill", + height: "fill", + child: await renderInvoiceList(selectedClientId, (id) => { + console.log("clicked invoice", id); + reDrawMain(selectedClientId, id.toString()); + }), + border: doubleBoxWithTitle(currentClientName), + }); + } + return InvoicesBox; +}; + +let Clients = new ScrollableList({ + minWidth: 20, + width: "natural", + cellForItem: (client: ClientResponse) => { + if (!client) { + // console.log("empty client company name", item); + return new Text({ text: `Undefined Client` }); + } + return new Button({ + border: "none", + width: "natural", + text: `${client.company}`, + onClick: async () => { + console.log("🚀 ~ clientInvoicesView ~ clicked client:", client); + reDrawMain(client.id); + }, + }); + }, + items: sorted, +}); + +const renderRightPanel = (box: Box) => + Flex.down({ + width: "fill", + height: "fill", + padding: { left: 1, right: 1 }, + children: [box], + }); + +export const clientInvoicesView = Flex.right({ + children: [Clients, renderRightPanel(await renderInvoicesBox())], +}); diff --git a/src/components/listInvoices.ts b/src/components/listInvoices.ts new file mode 100644 index 0000000..77cea35 --- /dev/null +++ b/src/components/listInvoices.ts @@ -0,0 +1,112 @@ +import { + Box, + Button, + Container, + Flex, + Flow, + Screen, + ScrollableList, + Text, +} from "wretched"; +import { + ClientResponse, + InvoiceResponse, + getAllClients, + getAllInvoices, + getAllProjects, +} from "../services/pancakeApi.js"; +import { compact, find, reverse, sortBy } from "lodash-es"; +import Table from "cli-table3"; +import { differenceInBusinessDays, format, parse } from "date-fns"; +import { tableCellBox } from "./helpers/tableCellBox.js"; +import { Theme } from "wretched/dist/Theme.js"; +import { + distanceInBizDays, + parseServiceDateString, + simpleFormat, +} from "./helpers/dates.js"; + +export const renderInvoiceList = async ( + clientId: string, + onSelect: (selectedInvoiceId: string | number) => void, +) => { + if (clientId === undefined) { + console.warn("No Client Id"); + return new Text({ text: " --" }); + } + const invResp = await getAllInvoices({ client_id: clientId }); + const invoices: Array = invResp.invoices + .sort((a, b) => Number(a.invoice_number) - Number(b.invoice_number)) + .reverse(); + + if (!invoices?.length) { + console.warn("No invoices"); + return new Text({ text: " --" }); + } + + type FormattedInvoice = InvoiceResponse & { dueDistance: string }; + const transformInvoice = (invoice: InvoiceResponse): FormattedInvoice => { + const ret: FormattedInvoice = { ...invoice, dueDistance: "" }; + try { + if (invoice.due_date?.length > 0) { + const due = parseServiceDateString(invoice.due_date); + ret.due_date = simpleFormat(due); + ret.dueDistance = `Due in ${distanceInBizDays(due)} biz days.`; + } + if (invoice.payment_date?.length > 0) { + ret.payment_date = simpleFormat( + parseServiceDateString(invoice.payment_date), + ); + } + ret.amount = "$" + Number(invoice.amount).toFixed(2).padEnd(2, "0"); + } catch (error) { + console.error("Error transforming date", error); + } finally { + return ret; + } + }; + // const detailsTable = new Table({ + // head: ["Invoice#", "Amount", "Due", "Paid?", "Overdue?"], + // }); + // const detailsTable: string[] | undefined = new Array(); + // detailsTable.concat( + // ...invoices.map((x) => [ + // x.invoice_number, + // Number(x.amount).toFixed(2), + // x.due_date?.substring(0, 10), + // x.is_paid ? "💰" : "⏳", + // x.overdue ? "🔥" : "✅", + // ]), + // ); + + return new ScrollableList({ + cellForItem: (item, row) => { + return Flex.right({ + children: [ + new Button({ + onClick: () => onSelect(item.id), + border: "none", + width: 6, + text: item.invoice_number, + }), + tableCellBox(item.amount, { + theme: item.is_paid ? Theme.green : Theme.secondary, + padding: { left: 1, right: 1, top: 0, bottom: 0 }, + borders: { right: false }, + width: 15, + alignment: "right", + }), + tableCellBox(item.dueDistance, { + theme: !item.is_paid && item.overdue ? Theme.red : Theme.secondary, + borders: { right: false }, + padding: { left: 1, right: 1, top: 0, bottom: 0 }, + width: 20, + alignment: "center", + }), + ], + }); + }, + items: invoices.map(transformInvoice), + showScrollbars: undefined, + }); +}; diff --git a/src/components/mainNav.ts b/src/components/mainNav.ts new file mode 100644 index 0000000..a439ad1 --- /dev/null +++ b/src/components/mainNav.ts @@ -0,0 +1,38 @@ +import { Box, Button, Flex, Text } from "wretched"; +import { Style } from "wretched/dist/Style.js"; +const tabs = { + Home: "/", + Clients: "/", + Invoices: "/", + Projects: "/", +} as const; +export const mainNav = (props: { activeTabName: keyof typeof tabs }) => { + return new Flex({ + direction: "leftToRight", + children: Object.keys(tabs).map( + (x) => + new Box({ + padding: { top: 0, bottom: 0, left: 2, right: 2 }, + // child: new Text({ text: x, style: new Style({ bold: true }) }), + child: new Button({ + text: x, + border: "none", + theme: props.activeTabName === x ? "plain" : "selected", + onClick: () => { + console.log({ clicked: x }); + }, + }), + border: [ + "", + "", + "", + "", + "", + "", + props.activeTabName === x ? "━" : "─", + "", + ], + }), + ), + }); +}; diff --git a/src/index.ts b/src/index.ts index 9c3d0bf..ef3bfb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,38 +1,47 @@ -import 'dotenv/config' -import { Screen, Box, Flow, Text, Button, interceptConsoleLog, ConsoleLog, iTerm2, Window, Flex } from 'wretched' +import "dotenv/config"; +import { + Screen, + Box, + Flow, + Text, + Button, + interceptConsoleLog, + ConsoleLog, + iTerm2, + Window, + Flex, +} from "wretched"; import * as utility from "wretched/dist/components/utility"; -import { clientInvoicesView } from "./components/listClientInvoices.js"; - - +import { clientInvoicesView } from "./components/listClients.js"; +import { mainNav } from "./components/mainNav.js"; +import { inspect } from "node:util"; interceptConsoleLog(); -process.title = 'Wretched'; +process.title = "Wretched"; const consoleLog = new ConsoleLog({ height: 12, -}) -const [screen, program] = await Screen.start( - async (program) => { - await iTerm2.setBackground(program, [23, 23, 23]) +}); +const [screen, program] = await Screen.start(async (program) => { + await iTerm2.setBackground(program, [23, 23, 23]); - return new Window({ - child: new utility.TrackMouse({ - content: Flex.down({ - children: [ - ['flex1', clientInvoicesView], - ['natural', consoleLog], - ], - }), - }), - }) - }, -) + return new Window({ + child: Flex.down({ + padding: { top: 1 }, + children: [ + ["natural", mainNav({ activeTabName: "Clients" })], + ["flex1", clientInvoicesView], + ["natural", consoleLog], + ], + }), + }); +}); -program.key('escape', function () { - consoleLog.clear() - screen.render() -}) +program.key("escape", function () { + consoleLog.clear(); + screen.render(); +}); -process.on("beforeExit", () => { - -}) +process.on("uncaughtException", (error) => { + console.error("\r\nUncaught Exception was Caught!", inspect(error)); +}); diff --git a/src/services/invoiceResponse.ts b/src/services/invoiceResponse.ts new file mode 100644 index 0000000..843c192 --- /dev/null +++ b/src/services/invoiceResponse.ts @@ -0,0 +1,146 @@ +export interface Item { + id: string; + unique_id: string; + name: string; + description: string; + qty: string; + rate: string; + period: string; + total: string; + sort: string; + type: string; + item_type_id: string; + discount: string; + discount_is_percentage: string; + item_type_table: string; + currency_code: null; + taxes: any[]; + tax_ids: any[]; + is_taxable: boolean; + tax_total: number; + billable_total: number; + tax_label: string; + taxes_buffer: any[]; + total_pre_tax_post_discounts: number; +} + +export interface PartialPayment { + id: string; + unique_invoice_id: string; + amount: string; + gateway_surcharge: string; + is_percentage: string; + due_date: string; + notes: string; + txn_id: string; + payment_gross: string; + item_name: string; + is_paid: string; + payment_date: string; + payment_type: string; + payer_status: string; + payment_status: string; + unique_id: string; + payment_method: string; + key: string; + improved: string; + transaction_fee: string; + due_date_input: string; + payment_url: string; + over_due: boolean; + billableAmount: number; +} + +export interface PartialPayments { + [key: string]: PartialPayment; +} + +export interface Items { + [key: string]: Item; +} + +export interface InvoiceDetailsResponse { + date_to_automatically_notify: string; + computed_due_date: string; + real_invoice_id: string; + real_invoice_unique_id: string; + paid: string; + overdue: string; + id: string; + unique_id: string; + client_id: string; + amount: string; + due_date: string; + invoice_number: string; + notes: null; + description: string; + txn_id: string; + payment_gross: string; + item_name: string; + payment_hash: string; + payment_status: string; + payment_type: string; + payment_date: string; + payer_status: string; + type: string; + date_entered: string; + is_paid: string; + is_recurring: string; + frequency: string; + auto_send: string; + recur_id: string; + currency_id: string; + exchange_rate: string; + proposal_id: string; + send_x_days_before: string; + has_sent_notification: string; + last_sent: string; + next_recur_date: string; + last_viewed: string; + is_viewable: string; + is_archived: string; + owner_id: string; + last_status_change: string; + status: string; + project_id: string; + auto_charge: string; + address: string; + language: string; + first_name: string; + last_name: string; + email: string; + company: string; + phone: string; + client_unique_id: string; + currency_code: string; + has_files: string; + total_comments: string; + tax_total: number; + billable_amount: number; + client_name: string; + formatted_is_paid: string; + days_overdue: number; + url: string; + currency_symbol: string; + part_count: string; + paid_part_count: string; + unpaid_part_count: string; + proposal_num: string; + list_invoice_belongs_to: string; + items: Items; + taxes: any[]; + sub_total: number; + has_discount: boolean; + discounts: any[]; + sub_total_after_discounts: number; + total: number; + receipts: any[]; + paid_amount: number; + unpaid_amount: number; + collected_taxes: any[]; + has_tax_reg: boolean; + tax_collected: number; + total_transaction_fees: number; + partial_payments: PartialPayments; + next_part_to_pay: number; +} diff --git a/src/services/pancakeApi.ts b/src/services/pancakeApi.ts index b5e5b9d..dcceb9e 100644 --- a/src/services/pancakeApi.ts +++ b/src/services/pancakeApi.ts @@ -1,25 +1,34 @@ - -import ky from 'ky'; - +import ky from "ky"; +import { InvoiceDetailsResponse } from "./invoiceResponse.js"; const API_KEY = process.env.PANCAKE_API_KEY; const API_URL = process.env.PANCAKE_API_URL; -console.log("🚀 ~ API_URL:", API_URL) +console.log("🚀 ~ API_URL:", API_URL); -const api = ky.create({ prefixUrl: API_URL, headers: { "x-api-key": `${API_KEY}` } }); +const api = ky.create({ + prefixUrl: API_URL, + headers: { "x-api-key": `${API_KEY}` }, +}); -type PaginationParams = { limit?: number, start?: number, sort_by?: string, sort_dir?: 'asc' | 'desc' } +type PaginationParams = { + limit?: number; + start?: number; + sort_by?: string; + sort_dir?: "asc" | "desc"; +}; export type ClientResponse = { - id: string, - first_name: string, - last_name: string, - company: string, - total: number, - unpaid_total: number -} + id: string; + first_name: string; + last_name: string; + company: string; + total: number; + unpaid_total: number; +}; // Clients -async function getAllClients(params: PaginationParams): Promise> { - const { limit = 100, start = 0, sort_by = 'id', sort_dir = 'desc' } = params; +async function getAllClients( + params: PaginationParams, +): Promise> { + const { limit = 100, start = 0, sort_by = "id", sort_dir = "desc" } = params; const url = `clients?limit=${limit}&start=${start}&sort_by=${sort_by}&sort_dir=${sort_dir}`; const response = api.get(url); return response.json(); @@ -34,7 +43,7 @@ async function getOneClient(id: string) { async function createNewClient(data) { const url = `clients/new`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -42,7 +51,7 @@ async function createNewClient(data) { async function updateClient(data) { const url = `clients/edit`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -50,18 +59,18 @@ async function updateClient(data) { async function deleteClient(id: string) { const url = `clients/delete`; const response = await api.post(url, { - json: { id } + json: { id }, }); return response.json(); } type ListResponse = { - status: boolean, - message: string, - count: number, -} & Record> + status: boolean; + message: string; + count: number; +} & Record>; type ProjectResponse = { - name: string -} + name: string; +}; // Projects async function getOneProject(id: string): Promise { const url = `projects/show?id=${id}`; @@ -69,20 +78,21 @@ async function getOneProject(id: string): Promise { return response.json(); } -async function getAllProjects(params: PaginationParams): Promise> { - const { limit = 5, start = 0, sort_by = 'id', sort_dir = 'asc' } = params; +async function getAllProjects( + params: PaginationParams, +): Promise> { + const { limit = 5, start = 0, sort_by = "id", sort_dir = "asc" } = params; const url = `projects?limit=${limit}&start=${start}&sort_by=${sort_by}&sort_dir=${sort_dir}`; const response = await api.get(url); return response.json(); } - // Projects (continued) async function createNewProject(data) { const url = `projects/new`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -90,7 +100,7 @@ async function createNewProject(data) { async function updateProject(data) { const url = `projects/edit`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -105,7 +115,7 @@ async function getTasksByProject(projectId: string) { async function createTask(data) { const url = `projects/tasks/new`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -113,7 +123,7 @@ async function createTask(data) { async function updateTask(data) { const url = `projects/tasks/update`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -121,11 +131,11 @@ async function updateTask(data) { async function deleteTask(taskId: string) { const url = `projects/tasks/show`; const response = await fetch(url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, - body: JSON.stringify({ id: taskId }) + body: JSON.stringify({ id: taskId }), }); return response.json(); } @@ -133,7 +143,7 @@ async function deleteTask(taskId: string) { async function logTimeOnTask(data) { const url = `projects/tasks/log_time`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -141,7 +151,7 @@ async function logTimeOnTask(data) { async function completeTask(taskId: string) { const url = `projects/tasks/compete`; const response = await api.post(url, { - json: { id: taskId } + json: { id: taskId }, }); return response.json(); } @@ -149,22 +159,62 @@ async function completeTask(taskId: string) { async function reopenTask(taskId: string) { const url = `projects/tasks/reopen`; const response = await api.post(url, { - json: { id: taskId } + json: { id: taskId }, }); return response.json(); } - -export type InvoiceResponse = { invoice_number: string, client_id: string, amount: string } +export type binaryBooleanString = "0" | "1"; +export type InvoiceResponse = { + id: number; + invoice_number: string; + client_id: string; + amount: string; + due_date: string; + description: string; + is_viewable: binaryBooleanString; + has_sent_notification: binaryBooleanString; + is_paid: boolean; + date_entered: string; + overdue: boolean; + payment_status: string; + payment_date: string; + company: string; + email: string; + /** + * "last_sent": "1527114961", + */ + last_sent: string; + /** + * "last_viewed": "1545454550", + */ + last_viewed: string; +}; // Invoices -async function getAllInvoices(params: { client_id: string } & PaginationParams): Promise> { - const { client_id, limit = 5, start = 0, sort_by = 'id', sort_dir = 'asc' } = params; - const queryParams = new URLSearchParams({ client_id, limit: limit.toFixed(0), start: start.toFixed(0), sort_by, sort_dir }); +async function getAllInvoices( + params: { client_id: string } & PaginationParams, +): Promise> { + const { + client_id, + limit = 0, + start = 0, + sort_by = "id", + sort_dir = "asc", + } = params; + const queryParams = new URLSearchParams({ + client_id, + limit: limit.toFixed(0), + start: start.toFixed(0), + sort_by, + sort_dir, + }); const url = `invoices?${queryParams.toString()}`; const response = await api.get(url); return response.json(); } -async function getOneInvoice(id: string): Promise { +async function getOneInvoice( + id: string, +): Promise<{ status: string; invoice: InvoiceDetailsResponse }> { const url = `invoices/show?id=${id}`; const response = await api.get(url); return response.json(); @@ -173,7 +223,7 @@ async function getOneInvoice(id: string): Promise { async function createNewInvoice(data) { const url = `invoices/new`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -181,7 +231,7 @@ async function createNewInvoice(data) { async function updateInvoice(data) { const url = `invoices/edit`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -189,7 +239,7 @@ async function updateInvoice(data) { async function deleteInvoice(invoiceId: string) { const url = `invoices/delete`; const response = await api.post(url, { - json: { id: invoiceId } + json: { id: invoiceId }, }); return response.json(); } @@ -197,7 +247,7 @@ async function deleteInvoice(invoiceId: string) { async function openInvoice(invoiceId: string) { const url = `invoices/open`; const response = await api.post(url, { - json: { id: invoiceId } + json: { id: invoiceId }, }); return response.json(); } @@ -205,7 +255,7 @@ async function openInvoice(invoiceId: string) { async function closeInvoice(invoiceId: string) { const url = `invoices/close`; const response = await api.post(url, { - json: { id: invoiceId } + json: { id: invoiceId }, }); return response.json(); } @@ -213,7 +263,7 @@ async function closeInvoice(invoiceId: string) { async function markInvoicePaid(invoiceId: string) { const url = `invoices/paid`; const response = await api.post(url, { - json: { id: invoiceId } + json: { id: invoiceId }, }); return response.json(); } @@ -221,7 +271,7 @@ async function markInvoicePaid(invoiceId: string) { async function sendInvoice(invoiceId: string) { const url = `invoices/send`; const response = await api.post(url, { - json: { id: invoiceId } + json: { id: invoiceId }, }); return response.json(); } @@ -234,8 +284,13 @@ async function getOneUser(id: string) { } async function getAllUsers(params: PaginationParams) { - const { limit = 5, start = 0, sort_by = 'id', sort_dir = 'asc' } = params; - const queryParams = new URLSearchParams({ limit: limit.toFixed(0), start: start.toFixed(0), sort_by, sort_dir }); + const { limit = 5, start = 0, sort_by = "id", sort_dir = "asc" } = params; + const queryParams = new URLSearchParams({ + limit: limit.toFixed(0), + start: start.toFixed(0), + sort_by, + sort_dir, + }); const url = `users?${queryParams.toString()}`; const response = await api.get(url); return response.json(); @@ -244,7 +299,7 @@ async function getAllUsers(params: PaginationParams) { async function updateUser(data) { const url = `users/edit`; const response = await api.post(url, { - json: data + json: data, }); return response.json(); } @@ -252,7 +307,7 @@ async function updateUser(data) { async function deleteUser(userId: string) { const url = `users/delete`; const response = await api.post(url, { - json: { id: userId } + json: { id: userId }, }); return response.json(); } @@ -264,13 +319,11 @@ export { createNewClient, updateClient, deleteClient, - // Projects getOneProject, getAllProjects, createNewProject, updateProject, - // Tasks getTasksByProject, createTask, @@ -279,7 +332,6 @@ export { logTimeOnTask, completeTask, reopenTask, - // Invoices getAllInvoices, getOneInvoice, @@ -290,10 +342,9 @@ export { closeInvoice, markInvoicePaid, sendInvoice, - // Users getOneUser, getAllUsers, updateUser, - deleteUser + deleteUser, };