Build Cobble project structure from the handoff
- Remove the preset document suite (letters, emails, invoices, quotes, PDF generation, line items) with its routes, API handlers and tables - Rebrand to Cobble: site data, Lato, handoff design tokens (light and dark) in main.css, brand assets in public/brand, ember stone logo and regenerated favicons - Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots) - Add the chat shell at /admin/bots: rail with pinned coordinator and user popover, bottom-anchored conversation with composer, working-on panel, and a send-message API scoped to the session user - Add BotStone and Wordmark components, crew status and relative time utils with tests, and update AGENTS.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabUserBot from "@/src/functions/backend/db/bots/grab-user-bot";
|
||||
import insertUserMessage from "@/src/functions/backend/db/bots/insert-user-message";
|
||||
import { SendBotMessageRequestSchema } from "@/src/schemas/bot-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Posts a message from the session user to one of their own bots.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (req.method != "POST") {
|
||||
throw new AppError(`Method not allowed`);
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status || !user.id) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (isReadOnlyUser({ user_types: user_types || [] })) {
|
||||
throw new AppError(`This user can only read records.`);
|
||||
}
|
||||
|
||||
const { bot_id, text } = parseWithSchema({
|
||||
schema: SendBotMessageRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id: user.id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found`);
|
||||
}
|
||||
|
||||
const message = await insertUserMessage({ bot_id: bot.id, text });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
singleRes: message,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { BUN_SQLITE_BSQLITE_INVOICES } from "@/db/types/db";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import grabNextDocumentSequence from "@/src/functions/backend/db/documents/grab-next-document-sequence";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
function invoiceNumberPrefix(): string {
|
||||
const now = new Date();
|
||||
|
||||
return `${SiteData["SiteSlug"].toUpperCase()}-${now.getFullYear()}${String(
|
||||
now.getMonth() + 1,
|
||||
).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
async function buildInvoiceRows({
|
||||
rows,
|
||||
}: {
|
||||
rows: BUN_SQLITE_BSQLITE_INVOICES[];
|
||||
}): Promise<BUN_SQLITE_BSQLITE_INVOICES[]> {
|
||||
const prefix = invoiceNumberPrefix();
|
||||
let next_sequence: number | undefined;
|
||||
const final_rows: BUN_SQLITE_BSQLITE_INVOICES[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const invoice_row = { ...row } as BUN_SQLITE_BSQLITE_INVOICES;
|
||||
|
||||
if (!invoice_row.invoice_number) {
|
||||
next_sequence ??= await grabNextDocumentSequence({
|
||||
table: "invoices",
|
||||
column: "invoice_number",
|
||||
prefix,
|
||||
});
|
||||
|
||||
invoice_row.invoice_number = `${prefix}-${String(next_sequence).padStart(3, "0")}`;
|
||||
next_sequence += 1;
|
||||
}
|
||||
|
||||
final_rows.push(invoice_row);
|
||||
}
|
||||
|
||||
return final_rows;
|
||||
}
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST": {
|
||||
const { table, body, user_types } = params;
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
if (!body?.insert_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No data to insert`,
|
||||
};
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const final_insert_data = await buildInvoiceRows({
|
||||
rows: body.insert_data as BUN_SQLITE_BSQLITE_INVOICES[],
|
||||
});
|
||||
|
||||
try {
|
||||
return await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body.update_on_duplicate,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (attempt >= 4) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Couldn't create invoice`);
|
||||
}
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { rmSync } from "fs";
|
||||
@@ -30,12 +30,12 @@ export default async function (
|
||||
}
|
||||
|
||||
let query = _.merge<
|
||||
ServerQueryParam<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>,
|
||||
ServerQueryParam<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>
|
||||
ServerQueryParam<BUN_SQLITE_COBBLE_ALL_TYPEDEFS>,
|
||||
ServerQueryParam<BUN_SQLITE_COBBLE_ALL_TYPEDEFS>
|
||||
>(body?.sql_query || {}, {});
|
||||
|
||||
const media_to_delete = await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_MEDIA,
|
||||
BUN_SQLITE_COBBLE_MEDIA,
|
||||
TableType
|
||||
>({
|
||||
table: "media",
|
||||
@@ -68,7 +68,7 @@ export default async function (
|
||||
});
|
||||
}
|
||||
|
||||
await BunSQLite.delete<BUN_SQLITE_BSQLITE_MEDIA, TableType>({
|
||||
await BunSQLite.delete<BUN_SQLITE_COBBLE_MEDIA, TableType>({
|
||||
table: "media",
|
||||
targetId: media.id,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
@@ -10,7 +10,7 @@ export default async function (
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types, user } = params;
|
||||
|
||||
let final_insert_data: BUN_SQLITE_BSQLITE_MEDIA[] = [
|
||||
let final_insert_data: BUN_SQLITE_COBBLE_MEDIA[] = [
|
||||
...(body?.insert_data || []),
|
||||
].map((d) => ({ ...d, user_id: user.id }));
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPost,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST":
|
||||
return await gatedCrudPost(params);
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { BUN_SQLITE_BSQLITE_QUOTES } from "@/db/types/db";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import grabNextDocumentSequence from "@/src/functions/backend/db/documents/grab-next-document-sequence";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import {
|
||||
gatedCrudDelete,
|
||||
gatedCrudGet,
|
||||
gatedCrudPut,
|
||||
} from "../../(utils)/gated-crud";
|
||||
|
||||
function quoteNumberPrefix(): string {
|
||||
const now = new Date();
|
||||
|
||||
return `${SiteData["SiteSlug"].toUpperCase()}Q-${now.getFullYear()}${String(
|
||||
now.getMonth() + 1,
|
||||
).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
async function buildQuoteRows({
|
||||
rows,
|
||||
}: {
|
||||
rows: BUN_SQLITE_BSQLITE_QUOTES[];
|
||||
}): Promise<BUN_SQLITE_BSQLITE_QUOTES[]> {
|
||||
const prefix = quoteNumberPrefix();
|
||||
let next_sequence: number | undefined;
|
||||
const final_rows: BUN_SQLITE_BSQLITE_QUOTES[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const quote_row = { ...row } as BUN_SQLITE_BSQLITE_QUOTES;
|
||||
|
||||
if (!quote_row.quote_number) {
|
||||
next_sequence ??= await grabNextDocumentSequence({
|
||||
table: "quotes",
|
||||
column: "quote_number",
|
||||
prefix,
|
||||
});
|
||||
|
||||
quote_row.quote_number = `${prefix}-${String(next_sequence).padStart(3, "0")}`;
|
||||
next_sequence += 1;
|
||||
}
|
||||
|
||||
final_rows.push(quote_row);
|
||||
}
|
||||
|
||||
return final_rows;
|
||||
}
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
switch (params.req.method) {
|
||||
case "GET":
|
||||
return await gatedCrudGet(params);
|
||||
case "POST": {
|
||||
const { table, body, user_types } = params;
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
if (!body?.insert_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No data to insert`,
|
||||
};
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const final_insert_data = await buildQuoteRows({
|
||||
rows: body.insert_data as BUN_SQLITE_BSQLITE_QUOTES[],
|
||||
});
|
||||
|
||||
try {
|
||||
return await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body.update_on_duplicate,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (attempt >= 4) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Couldn't create quote`);
|
||||
}
|
||||
case "PUT":
|
||||
return await gatedCrudPut(params);
|
||||
case "DELETE":
|
||||
return await gatedCrudDelete(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import {
|
||||
RolesAdminCantUpdate,
|
||||
SuperAdminOnlyEditableUserTypes,
|
||||
@@ -18,7 +18,7 @@ export default async function (
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
|
||||
const records_to_delete =
|
||||
await BunSQLite.select<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>({
|
||||
await BunSQLite.select<BUN_SQLITE_COBBLE_ALL_TYPEDEFS>({
|
||||
table,
|
||||
query: final_sql_query,
|
||||
targetId: id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import checkAllowedJoins from "@/src/functions/backend/db/check-allowed-joins";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
@@ -14,8 +14,8 @@ export default async function (
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
let final_query =
|
||||
_.merge<
|
||||
ApiReqParams<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>,
|
||||
ApiReqParams<BUN_SQLITE_BSQLITE_ALL_TYPEDEFS>
|
||||
ApiReqParams<BUN_SQLITE_COBBLE_ALL_TYPEDEFS>,
|
||||
ApiReqParams<BUN_SQLITE_COBBLE_ALL_TYPEDEFS>
|
||||
>(query || {}, { sql_query: final_sql_query }) || {};
|
||||
|
||||
const allowed_joins: TableType[] = ["user_types", "media"];
|
||||
|
||||
@@ -2,14 +2,6 @@ import _ from "lodash";
|
||||
import users from "./(tables)/users";
|
||||
import userTypes from "./(tables)/user_types";
|
||||
import media from "./(tables)/media";
|
||||
import letters from "./(tables)/letters";
|
||||
import emails from "./(tables)/emails";
|
||||
import emailAttachments from "./(tables)/email_attachments";
|
||||
import invoices from "./(tables)/invoices";
|
||||
import invoiceItems from "./(tables)/invoice_items";
|
||||
import quotes from "./(tables)/quotes";
|
||||
import quoteItems from "./(tables)/quote_items";
|
||||
import invoiceQuotes from "./(tables)/invoice_quotes";
|
||||
import get from "./(functions)/get";
|
||||
import put from "./(functions)/put";
|
||||
import del from "./(functions)/delete";
|
||||
@@ -91,22 +83,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
return await userTypes(crud_params);
|
||||
case "media":
|
||||
return await media(crud_params);
|
||||
case "letters":
|
||||
return await letters(crud_params);
|
||||
case "emails":
|
||||
return await emails(crud_params);
|
||||
case "email_attachments":
|
||||
return await emailAttachments(crud_params);
|
||||
case "invoices":
|
||||
return await invoices(crud_params);
|
||||
case "invoice_items":
|
||||
return await invoiceItems(crud_params);
|
||||
case "quotes":
|
||||
return await quotes(crud_params);
|
||||
case "quote_items":
|
||||
return await quoteItems(crud_params);
|
||||
case "invoice_quotes":
|
||||
return await invoiceQuotes(crud_params);
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
import TurndownService from "turndown";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import grabEmail from "@/src/functions/backend/db/documents/grab-email";
|
||||
import grabLetter from "@/src/functions/backend/db/documents/grab-letter";
|
||||
import grabEmailAttachments from "@/src/functions/backend/db/documents/grab-email-attachments";
|
||||
import grabInvoice, {
|
||||
grabInvoiceItems,
|
||||
} from "@/src/functions/backend/db/invoices/grab-invoice";
|
||||
import grabQuote, {
|
||||
grabQuoteItems,
|
||||
} from "@/src/functions/backend/db/quotes/grab-quote";
|
||||
import sendEmail from "@/src/functions/backend/email/send-email";
|
||||
import generateLetterPdf from "@/src/functions/backend/pdf/letter-pdf";
|
||||
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import path from "path";
|
||||
|
||||
const turndown_service = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
});
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const email_id = body.email_id;
|
||||
|
||||
if (!email_id) {
|
||||
throw new Error(`Email not found`);
|
||||
}
|
||||
|
||||
const email_res = await grabEmail({ id: email_id });
|
||||
const email = email_res.singleRes;
|
||||
|
||||
if (!email) {
|
||||
throw new Error(`Email not found`);
|
||||
}
|
||||
|
||||
if (email.status == "sent") {
|
||||
throw new Error(`This email has already been sent`);
|
||||
}
|
||||
|
||||
const to_emails = (email.to_emails || "").trim();
|
||||
|
||||
if (!to_emails) {
|
||||
throw new Error(
|
||||
`Please provide a recipient email address`,
|
||||
);
|
||||
}
|
||||
|
||||
let attachments: {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
contentType: string;
|
||||
}[] | undefined;
|
||||
|
||||
const attachments_res = await grabEmailAttachments({ email_id });
|
||||
const attachment_rows = attachments_res.payload || [];
|
||||
|
||||
if (attachment_rows[0]) {
|
||||
attachments = [];
|
||||
|
||||
for (const attachment of attachment_rows) {
|
||||
if (attachment.type == "letter" && attachment.letter_id) {
|
||||
const letter_res = await grabLetter({
|
||||
id: attachment.letter_id,
|
||||
});
|
||||
const letter = letter_res.singleRes;
|
||||
|
||||
if (!letter) {
|
||||
throw new Error(`Attached letter not found`);
|
||||
}
|
||||
|
||||
const pdf = await generateLetterPdf({
|
||||
template: letter,
|
||||
});
|
||||
|
||||
attachments.push({
|
||||
filename: `${twuiSlugify(letter.title || "letter")}.pdf`,
|
||||
content: Buffer.from(pdf),
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
} else if (attachment.type == "invoice" && attachment.invoice_id) {
|
||||
const invoice_res = await grabInvoice({
|
||||
id: attachment.invoice_id,
|
||||
});
|
||||
const invoice = invoice_res.singleRes;
|
||||
|
||||
if (!invoice) {
|
||||
throw new Error(`Attached invoice not found`);
|
||||
}
|
||||
|
||||
const items_res = await grabInvoiceItems({
|
||||
invoice_id: attachment.invoice_id,
|
||||
});
|
||||
const items = items_res.payload || [];
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice,
|
||||
items,
|
||||
});
|
||||
|
||||
attachments.push({
|
||||
filename: `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`,
|
||||
content: Buffer.from(pdf),
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
} else if (attachment.type == "quote" && attachment.quote_id) {
|
||||
const quote_res = await grabQuote({
|
||||
id: attachment.quote_id,
|
||||
});
|
||||
const quote = quote_res.singleRes;
|
||||
|
||||
if (!quote) {
|
||||
throw new Error(`Attached quote not found`);
|
||||
}
|
||||
|
||||
const items_res = await grabQuoteItems({
|
||||
quote_id: attachment.quote_id,
|
||||
});
|
||||
const items = items_res.payload || [];
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice: {
|
||||
...quote,
|
||||
invoice_number: quote.quote_number || "",
|
||||
},
|
||||
items,
|
||||
kind: "quote",
|
||||
});
|
||||
|
||||
attachments.push({
|
||||
filename: `${twuiSlugify(quote.quote_number || "quote")}.pdf`,
|
||||
content: Buffer.from(pdf),
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
} else if (attachment.media_write_path) {
|
||||
const { DATA_DIR } = grabDirNames();
|
||||
|
||||
const media_file = Bun.file(
|
||||
path.join(DATA_DIR, attachment.media_write_path),
|
||||
);
|
||||
|
||||
if (!media_file.size) {
|
||||
throw new Error(
|
||||
`Attachment file not found: ${attachment.file_name || "untitled"}`,
|
||||
);
|
||||
}
|
||||
|
||||
attachments.push({
|
||||
filename:
|
||||
attachment.file_name ||
|
||||
`${attachment.id || "attachment"}`,
|
||||
content: Buffer.from(await media_file.arrayBuffer()),
|
||||
contentType:
|
||||
attachment.mime_type ||
|
||||
attachment.media_mime_type ||
|
||||
"application/octet-stream",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachments[0]) {
|
||||
attachments = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const plain_body = turndown_service.turndown(
|
||||
email.body_html || "",
|
||||
);
|
||||
|
||||
const send_res = await sendEmail({
|
||||
to: to_emails,
|
||||
subject:
|
||||
email.subject ||
|
||||
email.title ||
|
||||
`Email from ${SiteData["SiteName"]}`,
|
||||
text: plain_body,
|
||||
content: (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: email.body_html || "",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: `${SiteData["SiteName"]}`,
|
||||
options: attachments ? { attachments } : undefined,
|
||||
});
|
||||
|
||||
if (!send_res.success) {
|
||||
return send_res;
|
||||
}
|
||||
|
||||
await BunSQLite.update({
|
||||
table: "emails",
|
||||
data: {
|
||||
status: "sent",
|
||||
sent_at: Date.now(),
|
||||
},
|
||||
targetId: email.id,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Email sent successfully to ${to_emails}`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
import type {
|
||||
BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS,
|
||||
} from "@/db/types/db";
|
||||
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { ApiReqParams, TableType } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export type EmailAttachmentDraft = {
|
||||
type: "letter" | "invoice" | "quote" | "file";
|
||||
letter_id?: number;
|
||||
invoice_id?: number;
|
||||
quote_id?: number;
|
||||
media_id?: number;
|
||||
file_name?: string;
|
||||
mime_type?: string;
|
||||
};
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const email_id = body.email_id;
|
||||
const attachments: EmailAttachmentDraft[] =
|
||||
(
|
||||
body as ApiReqParams & {
|
||||
attachments?: EmailAttachmentDraft[];
|
||||
}
|
||||
).attachments || [];
|
||||
|
||||
if (!email_id) {
|
||||
throw new Error(`Email not found`);
|
||||
}
|
||||
|
||||
const existing_res = await BunSQLite.select<BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS>({
|
||||
table: "email_attachments",
|
||||
query: {
|
||||
query: { email_id: { value: email_id } },
|
||||
},
|
||||
});
|
||||
|
||||
const existing = existing_res.payload || [];
|
||||
|
||||
const new_file_media_ids = attachments
|
||||
.filter((a) => a.type == "file" && a.media_id)
|
||||
.map((a) => Number(a.media_id));
|
||||
|
||||
for (const existing_attachment of existing) {
|
||||
if (
|
||||
existing_attachment.type == "file" &&
|
||||
existing_attachment.media_id &&
|
||||
!new_file_media_ids.includes(
|
||||
Number(existing_attachment.media_id),
|
||||
)
|
||||
) {
|
||||
await deleteMedia({
|
||||
ids: [existing_attachment.media_id],
|
||||
can_delete_all_media: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await BunSQLite.delete({
|
||||
table: "email_attachments",
|
||||
query: {
|
||||
query: { email_id: { value: email_id } },
|
||||
},
|
||||
});
|
||||
|
||||
if (attachments[0]) {
|
||||
const insert_data = attachments.map((attachment) => ({
|
||||
email_id: Number(email_id),
|
||||
type: attachment.type,
|
||||
letter_id:
|
||||
attachment.type == "letter" ? attachment.letter_id : undefined,
|
||||
invoice_id:
|
||||
attachment.type == "invoice"
|
||||
? attachment.invoice_id
|
||||
: undefined,
|
||||
quote_id:
|
||||
attachment.type == "quote" ? attachment.quote_id : undefined,
|
||||
media_id:
|
||||
attachment.type == "file" ? attachment.media_id : undefined,
|
||||
file_name: attachment.file_name,
|
||||
mime_type: attachment.mime_type,
|
||||
}));
|
||||
|
||||
const insert_res = await BunSQLite.insert<BUN_SQLITE_BSQLITE_EMAIL_ATTACHMENTS, TableType>({
|
||||
table: "email_attachments",
|
||||
data: insert_data,
|
||||
});
|
||||
|
||||
if (!insert_res.success) {
|
||||
throw new Error(insert_res.msg || `Couldn't save attachments`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Attachments saved`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import grabInvoice, {
|
||||
grabInvoiceItems,
|
||||
} from "@/src/functions/backend/db/invoices/grab-invoice";
|
||||
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const invoice_id = body?.invoice_id;
|
||||
|
||||
if (!invoice_id) {
|
||||
throw new Error(`Invoice not found`);
|
||||
}
|
||||
|
||||
const invoice_res = await grabInvoice({ id: invoice_id });
|
||||
const invoice = invoice_res.singleRes;
|
||||
|
||||
if (!invoice) {
|
||||
throw new Error(`Invoice not found`);
|
||||
}
|
||||
|
||||
const items_res = await grabInvoiceItems({
|
||||
invoice_id,
|
||||
});
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice,
|
||||
items: items_res.payload || [],
|
||||
});
|
||||
|
||||
const filename = `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`;
|
||||
|
||||
return new Response(new Uint8Array(pdf), {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition":
|
||||
`attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import grabInvoice, {
|
||||
grabInvoiceItems,
|
||||
} from "@/src/functions/backend/db/invoices/grab-invoice";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { InvoiceEmailContent } from "@/src/functions/backend/email/document-emails";
|
||||
import sendEmail from "@/src/functions/backend/email/send-email";
|
||||
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const invoice_id = body?.invoice_id;
|
||||
|
||||
if (!invoice_id) {
|
||||
throw new Error(`Invoice not found`);
|
||||
}
|
||||
|
||||
const invoice_res = await grabInvoice({ id: invoice_id });
|
||||
const invoice = invoice_res.singleRes;
|
||||
|
||||
if (!invoice) {
|
||||
throw new Error(`Invoice not found`);
|
||||
}
|
||||
|
||||
const to_email = body?.to_email || invoice.client_email;
|
||||
|
||||
if (!to_email) {
|
||||
throw new Error(
|
||||
`Please provide a recipient email address`,
|
||||
);
|
||||
}
|
||||
|
||||
const items_res = await grabInvoiceItems({
|
||||
invoice_id,
|
||||
});
|
||||
const items = items_res.payload || [];
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice,
|
||||
items,
|
||||
});
|
||||
|
||||
const pdf_filename = `${twuiSlugify(invoice.invoice_number || "invoice")}.pdf`;
|
||||
|
||||
const send_res = await sendEmail({
|
||||
to: to_email,
|
||||
subject: `Invoice ${invoice.invoice_number || ""} from ${SiteData["SiteName"]}`,
|
||||
text: `Please find attached invoice ${invoice.invoice_number || ""} for your records.`,
|
||||
content: (
|
||||
<InvoiceEmailContent
|
||||
invoice={invoice}
|
||||
items={items}
|
||||
/>
|
||||
),
|
||||
title: `${SiteData["SiteName"]}`,
|
||||
options: {
|
||||
attachments: [
|
||||
{
|
||||
filename: pdf_filename,
|
||||
content: Buffer.from(pdf),
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!send_res.success) {
|
||||
return send_res;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Invoice sent successfully to ${to_email}`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import grabLetter from "@/src/functions/backend/db/documents/grab-letter";
|
||||
import generateLetterPdf from "@/src/functions/backend/pdf/letter-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const letter_id = body?.letter_id;
|
||||
|
||||
if (!letter_id) {
|
||||
throw new Error(`Letter not found`);
|
||||
}
|
||||
|
||||
const letter_res = await grabLetter({ id: letter_id });
|
||||
|
||||
const letter = letter_res.singleRes;
|
||||
|
||||
if (!letter) {
|
||||
throw new Error(`Letter not found`);
|
||||
}
|
||||
|
||||
const pdf = await generateLetterPdf({
|
||||
template: letter,
|
||||
to_name: body?.to_name,
|
||||
to_address: body?.to_address,
|
||||
letter_date: body?.letter_date,
|
||||
});
|
||||
|
||||
const filename = `${twuiSlugify(letter.title || "letter")}.pdf`;
|
||||
|
||||
return new Response(new Uint8Array(pdf), {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition":
|
||||
`attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import grabQuote, {
|
||||
grabQuoteItems,
|
||||
} from "@/src/functions/backend/db/quotes/grab-quote";
|
||||
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const quote_id = body?.quote_id;
|
||||
|
||||
if (!quote_id) {
|
||||
throw new Error(`Quote not found`);
|
||||
}
|
||||
|
||||
const quote_res = await grabQuote({ id: quote_id });
|
||||
const quote = quote_res.singleRes;
|
||||
|
||||
if (!quote) {
|
||||
throw new Error(`Quote not found`);
|
||||
}
|
||||
|
||||
const items_res = await grabQuoteItems({
|
||||
quote_id,
|
||||
});
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice: {
|
||||
...quote,
|
||||
invoice_number: quote.quote_number || "",
|
||||
},
|
||||
items: items_res.payload || [],
|
||||
kind: "quote",
|
||||
});
|
||||
|
||||
const filename = `${twuiSlugify(quote.quote_number || "quote")}.pdf`;
|
||||
|
||||
return new Response(new Uint8Array(pdf), {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition":
|
||||
`attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
import grabQuote, {
|
||||
grabQuoteItems,
|
||||
} from "@/src/functions/backend/db/quotes/grab-quote";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { QuoteEmailContent } from "@/src/functions/backend/email/document-emails";
|
||||
import sendEmail from "@/src/functions/backend/email/send-email";
|
||||
import generateInvoicePdf from "@/src/functions/backend/pdf/invoice-pdf";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import twuiSlugify from "@moduletrace/twui/utils/slugify";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
try {
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
const access = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!access.success) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const quote_id = body?.quote_id;
|
||||
|
||||
if (!quote_id) {
|
||||
throw new Error(`Quote not found`);
|
||||
}
|
||||
|
||||
const quote_res = await grabQuote({ id: quote_id });
|
||||
const quote = quote_res.singleRes;
|
||||
|
||||
if (!quote) {
|
||||
throw new Error(`Quote not found`);
|
||||
}
|
||||
|
||||
const to_email = body?.to_email || quote.client_email;
|
||||
|
||||
if (!to_email) {
|
||||
throw new Error(
|
||||
`Please provide a recipient email address`,
|
||||
);
|
||||
}
|
||||
|
||||
const items_res = await grabQuoteItems({
|
||||
quote_id,
|
||||
});
|
||||
const items = items_res.payload || [];
|
||||
|
||||
const pdf = await generateInvoicePdf({
|
||||
invoice: {
|
||||
...quote,
|
||||
invoice_number: quote.quote_number || "",
|
||||
},
|
||||
items,
|
||||
kind: "quote",
|
||||
});
|
||||
|
||||
const pdf_filename = `${twuiSlugify(quote.quote_number || "quote")}.pdf`;
|
||||
|
||||
const send_res = await sendEmail({
|
||||
to: to_email,
|
||||
subject: `Quotation ${quote.quote_number || ""} from ${SiteData["SiteName"]}`,
|
||||
text: `Please find attached quotation ${quote.quote_number || ""} for our proposed works.`,
|
||||
content: (
|
||||
<QuoteEmailContent
|
||||
quote={quote}
|
||||
items={items}
|
||||
/>
|
||||
),
|
||||
title: `${SiteData["SiteName"]}`,
|
||||
options: {
|
||||
attachments: [
|
||||
{
|
||||
filename: pdf_filename,
|
||||
content: Buffer.from(pdf),
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!send_res.success) {
|
||||
return send_res;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Quotation sent successfully to ${to_email}`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user