Skip to content

Commit

Permalink
Feature/customers v1.2 (#344)
Browse files Browse the repository at this point in the history
* Add tags to customers

* Add tags to customers
  • Loading branch information
pontusab authored Dec 9, 2024
1 parent c1678ae commit 6b28809
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 55 deletions.
50 changes: 31 additions & 19 deletions apps/dashboard/src/actions/create-customer-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ export const createCustomerAction = authActionClient
channel: LogEvents.CreateCustomer.channel,
},
})
.action(async ({ parsedInput: input, ctx: { user, supabase } }) => {
const token = await generateToken(user.id);
.action(
async ({ parsedInput: { tags, ...input }, ctx: { user, supabase } }) => {
const token = await generateToken(user.id);

const { data } = await supabase
.from("customers")
.upsert(
{
...input,
token,
team_id: user.team_id,
},
{
onConflict: "id",
},
)
.select("id, name")
.single();
const { data } = await supabase
.from("customers")
.upsert(
{
...input,
token,
team_id: user.team_id,
},
{
onConflict: "id",
},
)
.select("id, name")
.single();

revalidateTag(`customers_${user.team_id}`);
if (tags?.length) {
await supabase.from("customer_tags").insert(
tags.map((tag) => ({
tag_id: tag.id,
customer_id: data?.id,
team_id: user.team_id!,
})),
);
}

return data;
});
revalidateTag(`customers_${user.team_id}`);

return data;
},
);
29 changes: 29 additions & 0 deletions apps/dashboard/src/actions/customer/create-customer-tag-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use server";

import { LogEvents } from "@midday/events/events";
import { revalidateTag } from "next/cache";
import { authActionClient } from "../safe-action";
import { createCustomerTagSchema } from "./schema";

export const createCustomerTagAction = authActionClient
.schema(createCustomerTagSchema)
.metadata({
name: "create-customer-tag",
track: {
event: LogEvents.CreateCustomerTag.name,
channel: LogEvents.CreateCustomerTag.channel,
},
})
.action(
async ({ parsedInput: { tagId, customerId }, ctx: { user, supabase } }) => {
const { data } = await supabase.from("customer_tags").insert({
tag_id: tagId,
customer_id: customerId,
team_id: user.team_id!,
});

revalidateTag(`customers_${user.team_id}`);

return data;
},
);
29 changes: 29 additions & 0 deletions apps/dashboard/src/actions/customer/delete-customer-tag-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use server";

import { LogEvents } from "@midday/events/events";
import { revalidateTag } from "next/cache";
import { authActionClient } from "../safe-action";
import { deleteCustomerTagSchema } from "./schema";

export const deleteCustomerTagAction = authActionClient
.schema(deleteCustomerTagSchema)
.metadata({
name: "delete-customer-tag",
track: {
event: LogEvents.DeleteCustomerTag.name,
channel: LogEvents.DeleteCustomerTag.channel,
},
})
.action(
async ({ parsedInput: { tagId, customerId }, ctx: { user, supabase } }) => {
const { data } = await supabase
.from("customer_tags")
.delete()
.eq("customer_id", customerId)
.eq("tag_id", tagId);

revalidateTag(`customers_${user.team_id}`);

return data;
},
);
11 changes: 11 additions & 0 deletions apps/dashboard/src/actions/customer/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from "zod";

export const deleteCustomerTagSchema = z.object({
tagId: z.string(),
customerId: z.string(),
});

export const createCustomerTagSchema = z.object({
tagId: z.string(),
customerId: z.string(),
});
9 changes: 9 additions & 0 deletions apps/dashboard/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,15 @@ export const createCustomerSchema = z.object({
website: z.string().nullable().optional(),
phone: z.string().nullable().optional(),
contact: z.string().nullable().optional(),
tags: z
.array(
z.object({
id: z.string().uuid(),
value: z.string(),
}),
)
.optional()
.nullable(),
});

export const inboxUploadSchema = z.array(
Expand Down
8 changes: 8 additions & 0 deletions apps/dashboard/src/components/charts/chart-period.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ const periods = [
to: new Date(),
},
},
{
value: "6m",
label: "Last 6 months",
range: {
from: subMonths(new Date(), 6),
to: new Date(),
},
},
{
value: "12m",
label: "Last 12 months",
Expand Down
98 changes: 96 additions & 2 deletions apps/dashboard/src/components/forms/customer-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";

import { createCustomerAction } from "@/actions/create-customer-action";
import { createCustomerTagAction } from "@/actions/customer/create-customer-tag-action";
import { deleteCustomerTagAction } from "@/actions/customer/delete-customer-tag-action";
import { useCustomerParams } from "@/hooks/use-customer-params";
import { useInvoiceParams } from "@/hooks/use-invoice-params";
import { zodResolver } from "@hookform/resolvers/zod";
Expand All @@ -14,12 +16,14 @@ import { Button } from "@midday/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@midday/ui/form";
import { Input } from "@midday/ui/input";
import { Label } from "@midday/ui/label";
import { SubmitButton } from "@midday/ui/submit-button";
import { Textarea } from "@midday/ui/textarea";
import { useAction } from "next-safe-action/hooks";
Expand All @@ -32,6 +36,7 @@ import {
type AddressDetails,
SearchAddressInput,
} from "../search-address-input";
import { SelectTags } from "../select-tags";
import { VatNumberInput } from "../vat-number-input";

const formSchema = z.object({
Expand All @@ -58,6 +63,15 @@ const formSchema = z.object({
zip: z.string().nullable().optional(),
vat_number: z.string().nullable().optional(),
note: z.string().nullable().optional(),
tags: z
.array(
z.object({
id: z.string().uuid(),
value: z.string(),
}),
)
.optional()
.nullable(),
});

const excludedDomains = [
Expand Down Expand Up @@ -86,6 +100,11 @@ export function CustomerForm({ data }: Props) {
const { setParams: setCustomerParams, name } = useCustomerParams();
const { setParams: setInvoiceParams } = useInvoiceParams();

const deleteCustomerTag = useAction(deleteCustomerTagAction);
const createCustomerTag = useAction(createCustomerTagAction);

const isEdit = !!data;

const createCustomer = useAction(createCustomerAction, {
onSuccess: ({ data }) => {
if (data) {
Expand All @@ -112,13 +131,22 @@ export function CustomerForm({ data }: Props) {
note: undefined,
phone: undefined,
contact: undefined,
tags: undefined,
},
});

useEffect(() => {
if (data) {
setSections(["general", "details"]);
form.reset(data);
form.reset({
...data,
tags:
data.tags?.map((tag) => ({
id: tag.tag?.id ?? "",
value: tag.tag?.name ?? "",
label: tag.tag?.name ?? "",
})) ?? undefined,
});
}
}, [data]);

Expand Down Expand Up @@ -392,6 +420,72 @@ export function CustomerForm({ data }: Props) {
/>
</div>

<div className="mt-6">
<Label
htmlFor="tags"
className="mb-2 text-xs text-[#878787] font-normal block"
>
Expense Tags
</Label>

<SelectTags
tags={form.getValues("tags")}
onRemove={(tag) => {
deleteCustomerTag.execute({
tagId: tag.id,
customerId: form.getValues("id")!,
});
}}
// Only for create customers
onCreate={(tag) => {
if (!isEdit) {
form.setValue(
"tags",
[
...(form.getValues("tags") ?? []),
{
value: tag.value ?? "",
id: tag.id ?? "",
},
],
{
shouldDirty: true,
shouldValidate: true,
},
);
}
}}
// Only for edit customers
onSelect={(tag) => {
if (isEdit) {
createCustomerTag.execute({
tagId: tag.id,
customerId: form.getValues("id")!,
});
} else {
form.setValue(
"tags",
[
...(form.getValues("tags") ?? []),
{
value: tag.value ?? "",
id: tag.id ?? "",
},
],
{
shouldDirty: true,
shouldValidate: true,
},
);
}
}}
/>

<FormDescription className="mt-2">
Tags help categorize and track customer expenses.
</FormDescription>
</div>

<div>
<FormField
control={form.control}
Expand Down Expand Up @@ -452,7 +546,7 @@ export function CustomerForm({ data }: Props) {
isSubmitting={createCustomer.isExecuting}
disabled={createCustomer.isExecuting || !form.formState.isValid}
>
{data ? "Update" : "Create"}
{isEdit ? "Update" : "Create"}
</SubmitButton>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/src/components/invoice/customer-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Customer {
vat?: string;
contact?: string;
website?: string;
tags?: { tag: { id: string; name: string } }[];
}

interface CustomerDetailsProps {
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/invoice/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function Form({ teamId, customers, onSubmit, isSubmitting }: Props) {
<>
{(draftInvoice.isPending || lastEditedText) && <span>-</span>}
<OpenURL
href={`/i/${token}`}
href={`${window.location.origin}/i/${token}`}
className="flex items-center gap-1"
>
<Icons.ExternalLink className="size-3" />
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/open-url.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function OpenURL({
}: { href: string; children: React.ReactNode; className?: string }) {
const handleOnClick = () => {
if (isDesktopApp()) {
platform.os.openURL(`${window.location.origin}/${href}`);
platform.os.openURL(href);
} else {
window.open(href, "_blank");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function InvoiceSheetContent({
</div>

<div className="flex mt-auto absolute bottom-6 justify-end gap-4 right-6 left-6">
<OpenURL href={`/i/${invoice.token}`}>
<OpenURL href={`${window.location.origin}/i/${invoice.token}`}>
<Button variant="secondary">View invoice</Button>
</OpenURL>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function TrackerUpdateSheet({ teamId, customers }: Props) {
</DropdownMenu>
</SheetHeader>

<ScrollArea className="h-full p-0 pb-280" hideScrollbar>
<ScrollArea className="h-full p-0 pb-28" hideScrollbar>
<TrackerProjectForm
form={form}
isSaving={updateAction.status === "executing"}
Expand Down
Loading

0 comments on commit 6b28809

Please sign in to comment.