This commit is contained in:
Tim Bendt
2024-11-01 11:32:37 -05:00
parent 22cb743835
commit fb36fb8303
12 changed files with 751 additions and 159 deletions

View File

@@ -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,
});
};

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
export const doubleBoxWithTitle = (
title: string,
): [string, string, string, string, string, string, string, string] => {
return ["═", "║", `╔═ ${title} `, "╗", "╚", "╝", "═", "║"];
};

View File

@@ -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);

View File

@@ -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 ? "|" : "",
],
});
};

View File

@@ -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<InvoiceResponse> = 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
],
}),
]
})

View File

@@ -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())],
});

View File

@@ -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<InvoiceResponse> = 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,
});
};

38
src/components/mainNav.ts Normal file
View File

@@ -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 ? "━" : "─",
"",
],
}),
),
});
};

View File

@@ -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));
});

View File

@@ -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;
}

View File

@@ -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<ListResponse<"clients", ClientResponse>> {
const { limit = 100, start = 0, sort_by = 'id', sort_dir = 'desc' } = params;
async function getAllClients(
params: PaginationParams,
): Promise<ListResponse<"clients", ClientResponse>> {
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<A extends string, T> = {
status: boolean,
message: string,
count: number,
} & Record<A, Array<T>>
status: boolean;
message: string;
count: number;
} & Record<A, Array<T>>;
type ProjectResponse = {
name: string
}
name: string;
};
// Projects
async function getOneProject(id: string): Promise<ProjectResponse> {
const url = `projects/show?id=${id}`;
@@ -69,20 +78,21 @@ async function getOneProject(id: string): Promise<ProjectResponse> {
return response.json();
}
async function getAllProjects(params: PaginationParams): Promise<ListResponse<"projects", ProjectResponse>> {
const { limit = 5, start = 0, sort_by = 'id', sort_dir = 'asc' } = params;
async function getAllProjects(
params: PaginationParams,
): Promise<ListResponse<"projects", ProjectResponse>> {
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<ListResponse<"invoices", InvoiceResponse>> {
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<ListResponse<"invoices", InvoiceResponse>> {
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<InvoiceResponse> {
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<InvoiceResponse> {
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,
};