mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 17:44:44 -05:00
Add default hourly rate to client model
The changes add a new defaultHourlyRate field to clients, including form updates and automatic rate propagation to invoices. Add default hourly rate for clients The subject line you provided already clearly expresses the changes, and no additional context is needed in the body, so I'll keep just the subject line.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { UserPlus, Mail, Phone, Save, Loader2, ArrowLeft } from "lucide-react";
|
import { UserPlus, Save, Loader2, ArrowLeft, DollarSign } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -12,6 +12,7 @@ import { Label } from "~/components/ui/label";
|
|||||||
import { FormSkeleton } from "~/components/ui/skeleton";
|
import { FormSkeleton } from "~/components/ui/skeleton";
|
||||||
import { AddressForm } from "~/components/forms/address-form";
|
import { AddressForm } from "~/components/forms/address-form";
|
||||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||||
|
import { NumberInput } from "~/components/ui/number-input";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import {
|
import {
|
||||||
formatPhoneNumber,
|
formatPhoneNumber,
|
||||||
@@ -35,6 +36,7 @@ interface FormData {
|
|||||||
state: string;
|
state: string;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
country: string;
|
country: string;
|
||||||
|
defaultHourlyRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormErrors {
|
interface FormErrors {
|
||||||
@@ -46,6 +48,7 @@ interface FormErrors {
|
|||||||
state?: string;
|
state?: string;
|
||||||
postalCode?: string;
|
postalCode?: string;
|
||||||
country?: string;
|
country?: string;
|
||||||
|
defaultHourlyRate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialFormData: FormData = {
|
const initialFormData: FormData = {
|
||||||
@@ -58,6 +61,7 @@ const initialFormData: FormData = {
|
|||||||
state: "",
|
state: "",
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
country: "United States",
|
country: "United States",
|
||||||
|
defaultHourlyRate: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ClientForm({ clientId, mode }: ClientFormProps) {
|
export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||||
@@ -108,11 +112,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
state: client.state ?? "",
|
state: client.state ?? "",
|
||||||
postalCode: client.postalCode ?? "",
|
postalCode: client.postalCode ?? "",
|
||||||
country: client.country ?? "United States",
|
country: client.country ?? "United States",
|
||||||
|
defaultHourlyRate: client.defaultHourlyRate ?? 100,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [client, mode]);
|
}, [client, mode]);
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
const handleInputChange = (field: string, value: string | number) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
|
|
||||||
@@ -225,7 +230,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle>Basic Information</CardTitle>
|
<CardTitle>Basic Information</CardTitle>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
Enter the client's primary details
|
Enter the client's primary details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +327,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle>Address</CardTitle>
|
<CardTitle>Address</CardTitle>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
Client's physical location
|
Client's physical location
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -341,6 +346,49 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Billing Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
|
||||||
|
<DollarSign className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>Billing Information</CardTitle>
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
|
Default billing rates for this client
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="defaultHourlyRate"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Default Hourly Rate
|
||||||
|
</Label>
|
||||||
|
<NumberInput
|
||||||
|
value={formData.defaultHourlyRate}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange("defaultHourlyRate", value)
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
prefix="$"
|
||||||
|
width="full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
{errors.defaultHourlyRate && (
|
||||||
|
<p className="text-destructive text-sm">
|
||||||
|
{errors.defaultHourlyRate}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Actions - original position */}
|
{/* Form Actions - original position */}
|
||||||
@@ -411,12 +459,16 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||||
<span className="hidden sm:inline">{mode === "create" ? "Creating..." : "Saving..."}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{mode === "create" ? "Creating..." : "Saving..."}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Save className="h-4 w-4 sm:mr-2" />
|
<Save className="h-4 w-4 sm:mr-2" />
|
||||||
<span className="hidden sm:inline">{mode === "create" ? "Create Client" : "Save Changes"}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{mode === "create" ? "Create Client" : "Save Changes"}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -281,6 +281,20 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
}
|
}
|
||||||
}, [businesses, formData.businessId, invoiceId]);
|
}, [businesses, formData.businessId, invoiceId]);
|
||||||
|
|
||||||
|
// Update default hourly rate when client changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (formData.clientId && clients) {
|
||||||
|
const selectedClient = clients.find((c) => c.id === formData.clientId);
|
||||||
|
if (selectedClient?.defaultHourlyRate) {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
defaultHourlyRate: selectedClient.defaultHourlyRate,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [formData.clientId, clients]);
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
const totals = React.useMemo(() => {
|
const totals = React.useMemo(() => {
|
||||||
const subtotal = formData.items.reduce(
|
const subtotal = formData.items.reduce(
|
||||||
@@ -339,7 +353,11 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
if (idx === 0) return; // Already at top
|
if (idx === 0) return; // Already at top
|
||||||
setFormData((prev) => {
|
setFormData((prev) => {
|
||||||
const newItems = [...prev.items];
|
const newItems = [...prev.items];
|
||||||
[newItems[idx - 1], newItems[idx]] = [newItems[idx], newItems[idx - 1]];
|
if (idx > 0 && idx < newItems.length) {
|
||||||
|
const temp = newItems[idx - 1]!;
|
||||||
|
newItems[idx - 1] = newItems[idx]!;
|
||||||
|
newItems[idx] = temp;
|
||||||
|
}
|
||||||
return { ...prev, items: newItems };
|
return { ...prev, items: newItems };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -349,7 +367,11 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
if (idx === formData.items.length - 1) return; // Already at bottom
|
if (idx === formData.items.length - 1) return; // Already at bottom
|
||||||
setFormData((prev) => {
|
setFormData((prev) => {
|
||||||
const newItems = [...prev.items];
|
const newItems = [...prev.items];
|
||||||
[newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]];
|
if (idx >= 0 && idx < newItems.length - 1) {
|
||||||
|
const temp = newItems[idx]!;
|
||||||
|
newItems[idx] = newItems[idx + 1]!;
|
||||||
|
newItems[idx + 1] = temp;
|
||||||
|
}
|
||||||
return { ...prev, items: newItems };
|
return { ...prev, items: newItems };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,13 +7,42 @@ import { TRPCError } from "@trpc/server";
|
|||||||
const createClientSchema = z.object({
|
const createClientSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required").max(255, "Name is too long"),
|
name: z.string().min(1, "Name is required").max(255, "Name is too long"),
|
||||||
email: z.string().email("Invalid email").optional().or(z.literal("")),
|
email: z.string().email("Invalid email").optional().or(z.literal("")),
|
||||||
phone: z.string().max(50, "Phone number is too long").optional().or(z.literal("")),
|
phone: z
|
||||||
addressLine1: z.string().max(255, "Address is too long").optional().or(z.literal("")),
|
.string()
|
||||||
addressLine2: z.string().max(255, "Address is too long").optional().or(z.literal("")),
|
.max(50, "Phone number is too long")
|
||||||
city: z.string().max(100, "City name is too long").optional().or(z.literal("")),
|
.optional()
|
||||||
state: z.string().max(50, "State name is too long").optional().or(z.literal("")),
|
.or(z.literal("")),
|
||||||
postalCode: z.string().max(20, "Postal code is too long").optional().or(z.literal("")),
|
addressLine1: z
|
||||||
country: z.string().max(100, "Country name is too long").optional().or(z.literal("")),
|
.string()
|
||||||
|
.max(255, "Address is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
addressLine2: z
|
||||||
|
.string()
|
||||||
|
.max(255, "Address is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
city: z
|
||||||
|
.string()
|
||||||
|
.max(100, "City name is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
state: z
|
||||||
|
.string()
|
||||||
|
.max(50, "State name is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
postalCode: z
|
||||||
|
.string()
|
||||||
|
.max(20, "Postal code is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
country: z
|
||||||
|
.string()
|
||||||
|
.max(100, "Country name is too long")
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("")),
|
||||||
|
defaultHourlyRate: z.number().min(0, "Rate must be positive").default(100),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateClientSchema = createClientSchema.partial().extend({
|
const updateClientSchema = createClientSchema.partial().extend({
|
||||||
@@ -23,10 +52,10 @@ const updateClientSchema = createClientSchema.partial().extend({
|
|||||||
export const clientsRouter = createTRPCRouter({
|
export const clientsRouter = createTRPCRouter({
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await ctx.db.query.clients.findMany({
|
return await ctx.db.query.clients.findMany({
|
||||||
where: eq(clients.createdById, ctx.session.user.id),
|
where: eq(clients.createdById, ctx.session.user.id),
|
||||||
orderBy: (clients, { desc }) => [desc(clients.createdAt)],
|
orderBy: (clients, { desc }) => [desc(clients.createdAt)],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
@@ -41,13 +70,13 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
const client = await ctx.db.query.clients.findFirst({
|
const client = await ctx.db.query.clients.findFirst({
|
||||||
where: eq(clients.id, input.id),
|
where: eq(clients.id, input.id),
|
||||||
with: {
|
with: {
|
||||||
invoices: {
|
invoices: {
|
||||||
orderBy: (invoices, { desc }) => [desc(invoices.createdAt)],
|
orderBy: (invoices, { desc }) => [desc(invoices.createdAt)],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -84,14 +113,17 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
Object.entries(input).map(([key, value]) => [
|
Object.entries(input).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
value === "" ? null : value,
|
value === "" ? null : value,
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [client] = await ctx.db.insert(clients).values({
|
const [client] = await ctx.db
|
||||||
name: input.name, // Ensure name is included
|
.insert(clients)
|
||||||
...cleanInput,
|
.values({
|
||||||
createdById: ctx.session.user.id,
|
name: input.name, // Ensure name is included
|
||||||
}).returning();
|
...cleanInput,
|
||||||
|
createdById: ctx.session.user.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -107,7 +139,7 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to create client",
|
message: "Failed to create client",
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -115,7 +147,7 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
.input(updateClientSchema)
|
.input(updateClientSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
const { id, ...data } = input;
|
const { id, ...data } = input;
|
||||||
|
|
||||||
// Verify client exists and belongs to user
|
// Verify client exists and belongs to user
|
||||||
const existingClient = await ctx.db.query.clients.findFirst({
|
const existingClient = await ctx.db.query.clients.findFirst({
|
||||||
@@ -141,15 +173,15 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
Object.entries(data).map(([key, value]) => [
|
Object.entries(data).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
value === "" ? null : value,
|
value === "" ? null : value,
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [updatedClient] = await ctx.db
|
const [updatedClient] = await ctx.db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
...cleanData,
|
...cleanData,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(clients.id, id))
|
.where(eq(clients.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -202,12 +234,13 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
if (clientInvoices.length > 0) {
|
if (clientInvoices.length > 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Cannot delete client with existing invoices. Please delete or reassign the invoices first.",
|
message:
|
||||||
|
"Cannot delete client with existing invoices. Please delete or reassign the invoices first.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.delete(clients).where(eq(clients.id, input.id));
|
await ctx.db.delete(clients).where(eq(clients.id, input.id));
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) throw error;
|
if (error instanceof TRPCError) throw error;
|
||||||
@@ -218,4 +251,4 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const clients = createTable(
|
|||||||
state: d.text({ length: 50 }),
|
state: d.text({ length: 50 }),
|
||||||
postalCode: d.text({ length: 20 }),
|
postalCode: d.text({ length: 20 }),
|
||||||
country: d.text({ length: 100 }),
|
country: d.text({ length: 100 }),
|
||||||
|
defaultHourlyRate: d.real().notNull().default(100.0),
|
||||||
createdById: d
|
createdById: d
|
||||||
.text({ length: 255 })
|
.text({ length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -124,7 +125,10 @@ export const clients = createTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const clientsRelations = relations(clients, ({ one, many }) => ({
|
export const clientsRelations = relations(clients, ({ one, many }) => ({
|
||||||
createdBy: one(users, { fields: [clients.createdById], references: [users.id] }),
|
createdBy: one(users, {
|
||||||
|
fields: [clients.createdById],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
invoices: many(invoices),
|
invoices: many(invoices),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -168,7 +172,10 @@ export const businesses = createTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const businessesRelations = relations(businesses, ({ one, many }) => ({
|
export const businessesRelations = relations(businesses, ({ one, many }) => ({
|
||||||
createdBy: one(users, { fields: [businesses.createdById], references: [users.id] }),
|
createdBy: one(users, {
|
||||||
|
fields: [businesses.createdById],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
invoices: many(invoices),
|
invoices: many(invoices),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -181,9 +188,7 @@ export const invoices = createTable(
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
invoiceNumber: d.text({ length: 100 }).notNull(),
|
invoiceNumber: d.text({ length: 100 }).notNull(),
|
||||||
businessId: d
|
businessId: d.text({ length: 255 }).references(() => businesses.id),
|
||||||
.text({ length: 255 })
|
|
||||||
.references(() => businesses.id),
|
|
||||||
clientId: d
|
clientId: d
|
||||||
.text({ length: 255 })
|
.text({ length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -192,7 +197,7 @@ export const invoices = createTable(
|
|||||||
dueDate: d.integer({ mode: "timestamp" }).notNull(),
|
dueDate: d.integer({ mode: "timestamp" }).notNull(),
|
||||||
status: d.text({ length: 50 }).notNull().default("draft"), // draft, sent, paid, overdue
|
status: d.text({ length: 50 }).notNull().default("draft"), // draft, sent, paid, overdue
|
||||||
totalAmount: d.real().notNull().default(0),
|
totalAmount: d.real().notNull().default(0),
|
||||||
taxRate: d.real().notNull().default(0.00),
|
taxRate: d.real().notNull().default(0.0),
|
||||||
notes: d.text({ length: 1000 }),
|
notes: d.text({ length: 1000 }),
|
||||||
createdById: d
|
createdById: d
|
||||||
.text({ length: 255 })
|
.text({ length: 255 })
|
||||||
@@ -214,9 +219,18 @@ export const invoices = createTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const invoicesRelations = relations(invoices, ({ one, many }) => ({
|
export const invoicesRelations = relations(invoices, ({ one, many }) => ({
|
||||||
business: one(businesses, { fields: [invoices.businessId], references: [businesses.id] }),
|
business: one(businesses, {
|
||||||
client: one(clients, { fields: [invoices.clientId], references: [clients.id] }),
|
fields: [invoices.businessId],
|
||||||
createdBy: one(users, { fields: [invoices.createdById], references: [users.id] }),
|
references: [businesses.id],
|
||||||
|
}),
|
||||||
|
client: one(clients, {
|
||||||
|
fields: [invoices.clientId],
|
||||||
|
references: [clients.id],
|
||||||
|
}),
|
||||||
|
createdBy: one(users, {
|
||||||
|
fields: [invoices.createdById],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
items: many(invoiceItems),
|
items: many(invoiceItems),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -251,5 +265,8 @@ export const invoiceItems = createTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const invoiceItemsRelations = relations(invoiceItems, ({ one }) => ({
|
export const invoiceItemsRelations = relations(invoiceItems, ({ one }) => ({
|
||||||
invoice: one(invoices, { fields: [invoiceItems.invoiceId], references: [invoices.id] }),
|
invoice: one(invoices, {
|
||||||
|
fields: [invoiceItems.invoiceId],
|
||||||
|
references: [invoices.id],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user