Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/customers #340

Merged
merged 3 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dashboard/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ export const createCustomerSchema = z.object({
note: z.string().nullable().optional(),
website: z.string().nullable().optional(),
phone: z.string().nullable().optional(),
contact: z.string().nullable().optional(),
});

export const inboxUploadSchema = z.array(
Expand Down
44 changes: 44 additions & 0 deletions apps/dashboard/src/app/[locale]/(app)/(sidebar)/customers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CustomersHeader } from "@/components/customers-header";
import { ErrorFallback } from "@/components/error-fallback";
import { CustomersTable } from "@/components/tables/customers";
import { CustomersSkeleton } from "@/components/tables/customers/skeleton";
import type { Metadata } from "next";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { Suspense } from "react";
import { searchParamsCache } from "./search-params";

export const metadata: Metadata = {
title: "Customers | Midday",
};

export default async function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
const {
q: query,
sort,
start,
end,
page,
} = searchParamsCache.parse(searchParams);

return (
<div className="flex flex-col pt-6 gap-6">
<CustomersHeader />

<ErrorBoundary errorComponent={ErrorFallback}>
<Suspense fallback={<CustomersSkeleton />}>
<CustomersTable
query={query}
sort={sort}
start={start}
end={end}
page={page}
/>
</Suspense>
</ErrorBoundary>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
createSearchParamsCache,
parseAsArrayOf,
parseAsInteger,
parseAsString,
} from "nuqs/server";

export const searchParamsCache = createSearchParamsCache({
page: parseAsInteger.withDefault(0),
q: parseAsString.withDefault(""),
sort: parseAsArrayOf(parseAsString),
start: parseAsString,
end: parseAsString,
});
14 changes: 14 additions & 0 deletions apps/dashboard/src/components/customers-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { OpenCustomerSheet } from "./open-customer-sheet";
import { SearchField } from "./search-field";

export async function CustomersHeader() {
return (
<div className="flex items-center justify-between">
<SearchField placeholder="Search customers" />

<div className="hidden sm:block">
<OpenCustomerSheet />
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions apps/dashboard/src/components/forms/customer-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const formSchema = z.object({
.nullable()
.optional()
.transform((url) => url?.replace(/^https?:\/\//, "")),
contact: z.string().nullable().optional(),
address_line_1: z.string().nullable().optional(),
address_line_2: z.string().nullable().optional(),
city: z.string().nullable().optional(),
Expand Down Expand Up @@ -110,6 +111,7 @@ export function CustomerForm({ data }: Props) {
vat_number: undefined,
note: undefined,
phone: undefined,
contact: undefined,
},
});

Expand Down Expand Up @@ -237,6 +239,25 @@ export function CustomerForm({ data }: Props) {
</FormItem>
)}
/>

<FormField
control={form.control}
name="contact"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs text-[#878787] font-normal">
Contact person
</FormLabel>
<FormControl>
<Input
{...field}
placeholder="John Doe"
autoComplete="off"
/>
</FormControl>
</FormItem>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/components/invoice/customer-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface Customer {
zip?: string;
country?: string;
vat?: string;
contact?: string;
website?: string;
}

interface CustomerDetailsProps {
Expand Down
7 changes: 6 additions & 1 deletion apps/dashboard/src/components/main-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const icons = {
"/transactions": () => <Icons.Transactions size={22} />,
"/invoices": () => <Icons.Invoice size={22} />,
"/tracker": () => <Icons.Tracker size={22} />,
"/vault": () => <Icons.Files size={22} />,
"/customers": () => <Icons.Customers size={22} />,
"/vault": () => <Icons.Vault size={22} />,
"/settings": () => <Icons.Settings size={22} />,
"/apps": () => <Icons.Apps size={22} />,
"/inbox": () => <Icons.Inbox2 size={22} />,
Expand All @@ -51,6 +52,10 @@ const defaultItems = [
path: "/tracker",
name: "Tracker",
},
{
path: "/customers",
name: "Customers",
},
{
path: "/vault",
name: "Vault",
Expand Down
22 changes: 22 additions & 0 deletions apps/dashboard/src/components/open-customer-sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { useCustomerParams } from "@/hooks/use-customer-params";
import { useInvoiceParams } from "@/hooks/use-invoice-params";
import { Button } from "@midday/ui/button";
import { Icons } from "@midday/ui/icons";

export function OpenCustomerSheet() {
const { setParams } = useCustomerParams();

return (
<div>
<Button
variant="outline"
size="icon"
onClick={() => setParams({ createCustomer: true })}
>
<Icons.Add />
</Button>
</div>
);
}
6 changes: 5 additions & 1 deletion apps/dashboard/src/components/sheets/customer-edit-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export function CustomerEditSheet() {
}
}

fetchCustomer();
if (customerId) {
fetchCustomer();
} else {
setCustomer(null);
}
}, [customerId, supabase]);

return (
Expand Down
144 changes: 144 additions & 0 deletions apps/dashboard/src/components/tables/customers/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"use client";

import { useCustomerParams } from "@/hooks/use-customer-params";
import { Avatar, AvatarFallback, AvatarImageNext } from "@midday/ui/avatar";
import { Button } from "@midday/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@midday/ui/dropdown-menu";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import type { ColumnDef } from "@tanstack/react-table";
import Link from "next/link";
import * as React from "react";

export type Customer = {
id: string;
name: string;
customer_name?: string;
website: string;
contact?: string;
email: string;
invoices: { id: string }[];
projects: { id: string }[];
tags: { id: string; name: string }[];
};

export const columns: ColumnDef<Customer>[] = [
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => {
const name = row.original.name ?? row.original.customer_name;

if (!name) return "-";

return (
<div className="flex items-center space-x-2">
<Avatar className="size-5">
{row.original.website && (
<AvatarImageNext
src={`https://img.logo.dev/${row.original.website}?token=pk_X-1ZO13GSgeOoUrIuJ6GMQ&size=60`}
alt={`${name} logo`}
width={20}
height={20}
quality={100}
/>
)}
<AvatarFallback className="text-[9px] font-medium">
{name?.[0]}
</AvatarFallback>
</Avatar>
<span className="truncate">{name}</span>
</div>
);
},
},
{
header: "Contact person",
accessorKey: "contact",
cell: ({ row }) => row.getValue("contact") ?? "-",
},
{
header: "Email",
accessorKey: "email",
cell: ({ row }) => row.getValue("email") ?? "-",
},
{
header: "Invoices",
accessorKey: "invoices",
cell: ({ row }) => {
if (row.original.invoices.length > 0) {
return (
<Link href={`/invoices?customers=${row.original.id}`}>
{row.original.invoices.length}
</Link>
);
}

return "-";
},
},
{
header: "Projects",
accessorKey: "projects",
cell: ({ row }) => {
if (row.original.projects.length > 0) {
return (
<Link href={`/tracker?customers=${row.original.id}`}>
{row.original.projects.length}
</Link>
);
}

return "-";
},
},
// {
// header: "Tags",
// accessorKey: "tags",
// cell: ({ row }) => row.getValue("tags") ?? "-",
// },
{
id: "actions",
header: "Actions",
cell: ({ row, table }) => {
const { setParams } = useCustomerParams();

return (
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="relative">
<Button variant="ghost" className="h-8 w-8 p-0">
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
setParams({
customerId: row.original.id,
})
}
>
Edit customer
</DropdownMenuItem>

<DropdownMenuItem
onClick={() =>
table.options.meta?.deleteCustomer(row.original.id)
}
className="text-[#FF3638]"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];
57 changes: 57 additions & 0 deletions apps/dashboard/src/components/tables/customers/empty-states.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import { useCustomerParams } from "@/hooks/use-customer-params";
import { Button } from "@midday/ui/button";

export function EmptyState() {
const { setParams } = useCustomerParams();

return (
<div className="flex items-center justify-center ">
<div className="flex flex-col items-center mt-40">
<div className="text-center mb-6 space-y-2">
<h2 className="font-medium text-lg">No customers</h2>
<p className="text-[#606060] text-sm">
You haven't created any customers yet. <br />
Go ahead and create your first one.
</p>
</div>

<Button
variant="outline"
onClick={() =>
setParams({
createCustomer: true,
})
}
>
Create customer
</Button>
</div>
</div>
);
}

export function NoResults() {
const { setParams } = useCustomerParams();

return (
<div className="flex items-center justify-center ">
<div className="flex flex-col items-center mt-40">
<div className="text-center mb-6 space-y-2">
<h2 className="font-medium text-lg">No results</h2>
<p className="text-[#606060] text-sm">
Try another search, or adjusting the filters
</p>
</div>

<Button
variant="outline"
onClick={() => setParams(null, { shallow: false })}
>
Clear filters
</Button>
</div>
</div>
);
}
Loading
Loading