[] = [
const status = row.getValue("status");
const hasNewMessages = status === "overdue";
const { setParams } = useInvoiceParams();
+ const updateInvoice = useAction(updateInvoiceAction);
return (
@@ -202,8 +208,12 @@ export const columns: ColumnDef[] = [
{(status === "overdue" || status === "unpaid") && (
deleteInvoice.execute({ id: row.original.id })}
+ onClick={() =>
+ updateInvoice.execute({
+ id: row.original.id,
+ status: "canceled",
+ })
+ }
className="text-[#FF3638]"
>
Cancel
diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts
index b714e06bda..33d31b0fc3 100644
--- a/apps/dashboard/src/middleware.ts
+++ b/apps/dashboard/src/middleware.ts
@@ -33,7 +33,8 @@ export async function middleware(request: NextRequest) {
if (
!session &&
newUrl.pathname !== "/login" &&
- !newUrl.pathname.includes("/report")
+ !newUrl.pathname.includes("/report") &&
+ !newUrl.pathname.includes("/i/")
) {
const encodedSearchParams = `${newUrl.pathname.substring(1)}${
newUrl.search
diff --git a/packages/invoice/package.json b/packages/invoice/package.json
index 1b6abbadd9..fc4591e165 100644
--- a/packages/invoice/package.json
+++ b/packages/invoice/package.json
@@ -8,9 +8,15 @@
"format": "biome format --write .",
"typecheck": "tsc --noEmit"
},
+ "exports": {
+ ".": "./src/index.tsx",
+ "./token": "./src/token/index.ts",
+ "./number": "./src/utils/number.ts"
+ },
"dependencies": {
"@react-pdf/renderer": "^4.0.0",
"date-fns": "^4.1.0",
+ "jose": "^5.9.6",
"qrcode": "^1.5.4"
}
}
diff --git a/packages/invoice/src/token/index.ts b/packages/invoice/src/token/index.ts
new file mode 100644
index 0000000000..50399db906
--- /dev/null
+++ b/packages/invoice/src/token/index.ts
@@ -0,0 +1,17 @@
+import * as jose from "jose";
+
+export async function verify(token: string) {
+ const secret = new TextEncoder().encode(process.env.INVOICE_JWT_SECRET);
+ const { payload } = await jose.jwtVerify(token, secret);
+
+ return payload;
+}
+
+export async function generateToken(id: string) {
+ const secret = new TextEncoder().encode(process.env.INVOICE_JWT_SECRET);
+ const token = await new jose.SignJWT({ id })
+ .setProtectedHeader({ alg: "HS256" })
+ .sign(secret);
+
+ return token;
+}
diff --git a/apps/dashboard/src/utils/invoice.test.ts b/packages/invoice/src/utils/number.test.ts
similarity index 95%
rename from apps/dashboard/src/utils/invoice.test.ts
rename to packages/invoice/src/utils/number.test.ts
index 8c09126361..41da13bf33 100644
--- a/apps/dashboard/src/utils/invoice.test.ts
+++ b/packages/invoice/src/utils/number.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, test } from "bun:test";
-import { generateInvoiceNumber } from "./invoice";
+import { generateInvoiceNumber } from "./number";
describe("Generate invoice number", () => {
it("should generate correct invoice number for count less than 10", () => {
diff --git a/apps/dashboard/src/utils/invoice.ts b/packages/invoice/src/utils/number.ts
similarity index 100%
rename from apps/dashboard/src/utils/invoice.ts
rename to packages/invoice/src/utils/number.ts
diff --git a/packages/supabase/src/queries/cached-queries.ts b/packages/supabase/src/queries/cached-queries.ts
index 3ae58b486f..f4b7cc5615 100644
--- a/packages/supabase/src/queries/cached-queries.ts
+++ b/packages/supabase/src/queries/cached-queries.ts
@@ -22,7 +22,7 @@ import {
getCategoriesQuery,
getCustomersQuery,
getExpensesQuery,
- getInvoiceNumberCountQuery,
+ getInvoiceNumberQuery,
getInvoiceSummaryQuery,
getInvoiceTemplatesQuery,
getInvoicesQuery,
@@ -559,7 +559,7 @@ export const getInvoiceTemplates = async () => {
)();
};
-export const getInvoiceNumberCount = async () => {
+export const getInvoiceNumber = async () => {
const supabase = createClient();
const user = await getUser();
const teamId = user?.data?.team_id;
@@ -570,11 +570,11 @@ export const getInvoiceNumberCount = async () => {
return unstable_cache(
async () => {
- return getInvoiceNumberCountQuery(supabase, teamId);
+ return getInvoiceNumberQuery(supabase, teamId);
},
- ["invoice_number_count", teamId],
+ ["invoice_number", teamId],
{
- tags: [`invoice_number_count_${teamId}`],
+ tags: [`invoice_number_${teamId}`],
revalidate: 3600,
},
)();
diff --git a/packages/supabase/src/queries/index.ts b/packages/supabase/src/queries/index.ts
index 6e79d8c0c3..4fe6459328 100644
--- a/packages/supabase/src/queries/index.ts
+++ b/packages/supabase/src/queries/index.ts
@@ -1,4 +1,5 @@
import { UTCDate } from "@date-fns/utc";
+import { generateInvoiceNumber } from "@midday/invoice/number";
import {
addDays,
endOfMonth,
@@ -1144,7 +1145,7 @@ export async function getInvoicesQuery(
const query = supabase
.from("invoices")
.select(
- "id, short_id, invoice_number, due_date, invoice_date, paid_at, updated_at, viewed_at, amount, template, currency, status, vat, tax, customer:customer_id(id, name, website)",
+ "id, invoice_number, token, due_date, invoice_date, paid_at, updated_at, viewed_at, amount, template, currency, status, vat, tax, customer:customer_id(id, name, website), customer_name",
{ count: "exact" },
)
.eq("team_id", teamId);
@@ -1231,6 +1232,7 @@ export async function getCustomersQuery(supabase: Client, teamId: string) {
.from("customers")
.select("*")
.eq("team_id", teamId)
+ .order("created_at", { ascending: false })
.limit(100);
}
@@ -1249,18 +1251,39 @@ export async function getInvoiceTemplatesQuery(
.single();
}
-export async function getInvoiceNumberCountQuery(
- supabase: Client,
- teamId: string,
-) {
- return supabase
+export async function getInvoiceNumberQuery(supabase: Client, teamId: string) {
+ const { count } = await supabase
.from("invoices")
.select("id", { count: "exact" })
.eq("team_id", teamId);
+
+ let nextNumber = generateInvoiceNumber(count || 0);
+ let isUnique = false;
+
+ while (!isUnique) {
+ const { data } = await supabase
+ .from("invoices")
+ .select("invoice_number")
+ .eq("team_id", teamId)
+ .eq("invoice_number", nextNumber)
+ .maybeSingle();
+
+ if (!data) {
+ isUnique = true;
+ } else {
+ nextNumber = generateInvoiceNumber((count || 0) + 1);
+ }
+ }
+
+ return nextNumber;
}
-export async function getInvoiceQuery(supabase: Client, invoiceId: string) {
- return supabase.from("invoices").select("*").eq("id", invoiceId).single();
+export async function getInvoiceQuery(supabase: Client, id: string) {
+ return supabase
+ .from("invoices")
+ .select("*, customer:customer_id(name), team:team_id(name)")
+ .eq("id", id)
+ .single();
}
type SearchInvoiceNumberParams = {
diff --git a/packages/supabase/src/types/db.ts b/packages/supabase/src/types/db.ts
index bb5a60d7d3..c3161705ff 100644
--- a/packages/supabase/src/types/db.ts
+++ b/packages/supabase/src/types/db.ts
@@ -207,6 +207,7 @@ export type Database = {
phone: string | null
state: string | null
team_id: string
+ token: string
vat_number: string | null
website: string | null
zip: string | null
@@ -225,6 +226,7 @@ export type Database = {
phone?: string | null
state?: string | null
team_id?: string
+ token?: string
vat_number?: string | null
website?: string | null
zip?: string | null
@@ -243,6 +245,7 @@ export type Database = {
phone?: string | null
state?: string | null
team_id?: string
+ token?: string
vat_number?: string | null
website?: string | null
zip?: string | null
@@ -450,11 +453,14 @@ export type Database = {
created_at: string
currency: string | null
customer_label: string | null
+ date_format: string | null
description_label: string | null
due_date_label: string | null
from_details: Json | null
from_label: string | null
id: string
+ include_tax: boolean | null
+ include_vat: boolean | null
invoice_no_label: string | null
issue_date_label: string | null
logo_url: string | null
@@ -463,7 +469,9 @@ export type Database = {
payment_details_label: string | null
price_label: string | null
quantity_label: string | null
+ size: Database["public"]["Enums"]["invoice_size"] | null
tax_label: string | null
+ tax_rate: number | null
team_id: string
total_label: string | null
vat_label: string | null
@@ -472,11 +480,14 @@ export type Database = {
created_at?: string
currency?: string | null
customer_label?: string | null
+ date_format?: string | null
description_label?: string | null
due_date_label?: string | null
from_details?: Json | null
from_label?: string | null
id?: string
+ include_tax?: boolean | null
+ include_vat?: boolean | null
invoice_no_label?: string | null
issue_date_label?: string | null
logo_url?: string | null
@@ -485,7 +496,9 @@ export type Database = {
payment_details_label?: string | null
price_label?: string | null
quantity_label?: string | null
+ size?: Database["public"]["Enums"]["invoice_size"] | null
tax_label?: string | null
+ tax_rate?: number | null
team_id: string
total_label?: string | null
vat_label?: string | null
@@ -494,11 +507,14 @@ export type Database = {
created_at?: string
currency?: string | null
customer_label?: string | null
+ date_format?: string | null
description_label?: string | null
due_date_label?: string | null
from_details?: Json | null
from_label?: string | null
id?: string
+ include_tax?: boolean | null
+ include_vat?: boolean | null
invoice_no_label?: string | null
issue_date_label?: string | null
logo_url?: string | null
@@ -507,7 +523,9 @@ export type Database = {
payment_details_label?: string | null
price_label?: string | null
quantity_label?: string | null
+ size?: Database["public"]["Enums"]["invoice_size"] | null
tax_label?: string | null
+ tax_rate?: number | null
team_id?: string
total_label?: string | null
vat_label?: string | null
@@ -525,25 +543,32 @@ export type Database = {
invoices: {
Row: {
amount: number | null
+ base_amount: number | null
+ base_currency: string | null
company_datails: Json | null
created_at: string
currency: string | null
- customer_datails: Json | null
+ customer_details: Json | null
customer_id: string | null
due_date: string | null
+ from_details: Json | null
fts: unknown | null
id: string
internal_note: string | null
invoice_date: string | null
invoice_number: string | null
+ issue_date: string | null
line_items: Json | null
note: string | null
+ note_details: Json | null
paid_at: string | null
path_tokens: string[] | null
- payment_datails: Json | null
+ payment_details: Json | null
+ short_id: string
status: Database["public"]["Enums"]["invoice_status"]
tax: number | null
team_id: string
+ template: Json | null
updated_at: string | null
url: string | null
vat: number | null
@@ -551,25 +576,32 @@ export type Database = {
}
Insert: {
amount?: number | null
+ base_amount?: number | null
+ base_currency?: string | null
company_datails?: Json | null
created_at?: string
currency?: string | null
- customer_datails?: Json | null
+ customer_details?: Json | null
customer_id?: string | null
due_date?: string | null
+ from_details?: Json | null
fts?: unknown | null
id?: string
internal_note?: string | null
invoice_date?: string | null
invoice_number?: string | null
+ issue_date?: string | null
line_items?: Json | null
note?: string | null
+ note_details?: Json | null
paid_at?: string | null
path_tokens?: string[] | null
- payment_datails?: Json | null
+ payment_details?: Json | null
+ short_id?: string
status?: Database["public"]["Enums"]["invoice_status"]
tax?: number | null
team_id: string
+ template?: Json | null
updated_at?: string | null
url?: string | null
vat?: number | null
@@ -577,25 +609,32 @@ export type Database = {
}
Update: {
amount?: number | null
+ base_amount?: number | null
+ base_currency?: string | null
company_datails?: Json | null
created_at?: string
currency?: string | null
- customer_datails?: Json | null
+ customer_details?: Json | null
customer_id?: string | null
due_date?: string | null
+ from_details?: Json | null
fts?: unknown | null
id?: string
internal_note?: string | null
invoice_date?: string | null
invoice_number?: string | null
+ issue_date?: string | null
line_items?: Json | null
note?: string | null
+ note_details?: Json | null
paid_at?: string | null
path_tokens?: string[] | null
- payment_datails?: Json | null
+ payment_details?: Json | null
+ short_id?: string
status?: Database["public"]["Enums"]["invoice_status"]
tax?: number | null
team_id?: string
+ template?: Json | null
updated_at?: string | null
url?: string | null
vat?: number | null
@@ -2015,6 +2054,7 @@ export type Database = {
connection_status: "disconnected" | "connected" | "unknown"
inbox_status: "processing" | "pending" | "archived" | "new" | "deleted"
inbox_type: "invoice" | "expense"
+ invoice_size: "a4" | "letter"
invoice_status: "draft" | "overdue" | "paid" | "unpaid" | "canceled"
reportTypes: "profit" | "revenue" | "burn_rate" | "expense"
teamRoles: "owner" | "member"
diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx
index be22379c38..d6ad2c467c 100644
--- a/packages/ui/src/components/sheet.tsx
+++ b/packages/ui/src/components/sheet.tsx
@@ -58,11 +58,11 @@ const SheetContent = React.forwardRef<
SheetContentProps
>(({ side = "right", stack = false, className, children, ...props }, ref) => (
- {!stack && }
+
e.preventDefault()}
ref={ref}
- className={cn("md:p-4", stack && "md:m-2", sheetVariants({ side }))}
+ className={cn("md:p-4", sheetVariants({ side }))}
aria-describedby={props["aria-describedby"] || undefined}
{...props}
>