wip
This commit is contained in:
82
src/components/detailsInvoice.ts
Normal file
82
src/components/detailsInvoice.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
42
src/components/helpers/animatedText.ts
Normal file
42
src/components/helpers/animatedText.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
src/components/helpers/boxBorders.ts
Normal file
5
src/components/helpers/boxBorders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const doubleBoxWithTitle = (
|
||||
title: string,
|
||||
): [string, string, string, string, string, string, string, string] => {
|
||||
return ["═", "║", `╔═ ${title} `, "╗", "╚", "╝", "═", "║"];
|
||||
};
|
||||
21
src/components/helpers/dates.ts
Normal file
21
src/components/helpers/dates.ts
Normal 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);
|
||||
53
src/components/helpers/tableCellBox.ts
Normal file
53
src/components/helpers/tableCellBox.ts
Normal 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 ? "|" : "",
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
],
|
||||
}),
|
||||
]
|
||||
})
|
||||
104
src/components/listClients.ts
Normal file
104
src/components/listClients.ts
Normal 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())],
|
||||
});
|
||||
112
src/components/listInvoices.ts
Normal file
112
src/components/listInvoices.ts
Normal 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
38
src/components/mainNav.ts
Normal 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 ? "━" : "─",
|
||||
"",
|
||||
],
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
59
src/index.ts
59
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({
|
||||
child: Flex.down({
|
||||
padding: { top: 1 },
|
||||
children: [
|
||||
['flex1', clientInvoicesView],
|
||||
['natural', consoleLog],
|
||||
["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));
|
||||
});
|
||||
|
||||
146
src/services/invoiceResponse.ts
Normal file
146
src/services/invoiceResponse.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user