diff --git a/client/src/components/customer/customer-list-card.tsx b/client/src/components/customer/customer-list-card.tsx new file mode 100644 index 0000000..dd193aa --- /dev/null +++ b/client/src/components/customer/customer-list-card.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import { format } from "date-fns"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { useCustomer } from "@/context/customer-context"; +import { Customer } from "@shared/schema"; +import { Calendar, Server, Users } from "lucide-react"; + +interface CustomerListCardProps { + customers: Customer[]; +} + +export function CustomerListCard({ customers }: CustomerListCardProps) { + const { currentCustomer, setCurrentCustomer } = useCustomer(); + + if (!customers || customers.length === 0) { + return ( + + + Customers + + You don't have any customers yet. + + + +
+

+ Create a customer in the settings page. +

+
+
+
+ ); + } + + return ( + + + Customers + + Select a customer to manage their domains and DNS records + + + +
+ {customers.map((customer) => ( +
setCurrentCustomer(customer)} + > +
+
+ + {customer.name} + {!customer.isActive && ( + + Inactive + + )} +
+
+
+
+ + 3 Members +
+
+ + Created {format(new Date(customer.createdAt), 'MMM d, yyyy')} +
+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/shared/customer-selector.tsx b/client/src/components/shared/customer-selector.tsx new file mode 100644 index 0000000..496e42a --- /dev/null +++ b/client/src/components/shared/customer-selector.tsx @@ -0,0 +1,63 @@ +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useCustomer } from "@/context/customer-context"; +import { ChevronDown } from "lucide-react"; + +export function CustomerSelector() { + const { customers, currentCustomer, setCurrentCustomer, isLoading } = useCustomer(); + + if (isLoading) { + return ( +
+
+ + Loading... +
+
+ ); + } + + if (!currentCustomer || customers.length === 0) { + return ( +
+
+ + No Customer +
+
+ ); + } + + return ( + + + + + + {customers.map((customer) => ( + setCurrentCustomer(customer)} + className={customer.id === currentCustomer.id ? "bg-muted" : ""} + > +
+ + {customer.name} +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/client/src/context/customer-context.tsx b/client/src/context/customer-context.tsx new file mode 100644 index 0000000..bb5f2e4 --- /dev/null +++ b/client/src/context/customer-context.tsx @@ -0,0 +1,153 @@ +import { createContext, ReactNode, useContext, useState, useEffect } from "react"; +import { useQuery, useMutation, UseMutationResult } from "@tanstack/react-query"; +import { Customer, Domain, InsertCustomer } from "@shared/schema"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import { useToast } from "@/hooks/use-toast"; +import { useAuth } from "@/hooks/use-auth"; + +type CustomerContextType = { + customers: Customer[]; + currentCustomer: Customer | null; + setCurrentCustomer: (customer: Customer) => void; + isLoading: boolean; + createCustomerMutation: UseMutationResult; + updateCustomerMutation: UseMutationResult }>; + deleteCustomerMutation: UseMutationResult; + domainsLoading: boolean; + domainsByCustomer: Record; +}; + +const CustomerContext = createContext(undefined); + +export function CustomerProvider({ children }: { children: ReactNode }) { + const { user } = useAuth(); + const { toast } = useToast(); + const [currentCustomer, setCurrentCustomer] = useState(null); + const [domainsByCustomer, setDomainsByCustomer] = useState>({}); + + // Get customers + const { data: customers = [], isLoading } = useQuery({ + queryKey: ["/api/customers"], + staleTime: 1000 * 60 * 5, // 5 minutes + }); + + // Load domains for current customer + const { isLoading: domainsLoading } = useQuery({ + queryKey: ["/api/domains", { customerId: currentCustomer?.id }], + enabled: !!currentCustomer, + staleTime: 1000 * 60 * 5, // 5 minutes + onSuccess: (domains) => { + if (currentCustomer) { + setDomainsByCustomer(prev => ({ + ...prev, + [currentCustomer.id]: domains + })); + } + } + }); + + // Customer mutations + const createCustomerMutation = useMutation({ + mutationFn: async (customer: InsertCustomer) => { + const res = await apiRequest("POST", "/api/customers", customer); + return await res.json(); + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["/api/customers"] }); + toast({ + title: "Customer created", + description: "The customer has been successfully created.", + }); + }, + onError: (error) => { + toast({ + title: "Failed to create customer", + description: error.message, + variant: "destructive", + }); + }, + }); + + const updateCustomerMutation = useMutation({ + mutationFn: async ({ id, data }: { id: string; data: Partial }) => { + const res = await apiRequest("PUT", `/api/customers/${id}`, data); + return await res.json(); + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["/api/customers"] }); + toast({ + title: "Customer updated", + description: "The customer has been successfully updated.", + }); + }, + onError: (error) => { + toast({ + title: "Failed to update customer", + description: error.message, + variant: "destructive", + }); + }, + }); + + const deleteCustomerMutation = useMutation({ + mutationFn: async (id: string) => { + await apiRequest("DELETE", `/api/customers/${id}`); + return true; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/customers"] }); + toast({ + title: "Customer deleted", + description: "The customer has been successfully deleted.", + }); + + // Reset current customer if it was deleted + if (currentCustomer && customers.length > 0 && !customers.find(c => c.id === currentCustomer.id)) { + setCurrentCustomer(customers[0]); + } + }, + onError: (error) => { + toast({ + title: "Failed to delete customer", + description: error.message, + variant: "destructive", + }); + }, + }); + + // Set default customer when customers are loaded + useEffect(() => { + if (customers.length > 0 && !currentCustomer) { + // Otherwise, use the first customer + setCurrentCustomer(customers[0]); + } + }, [customers, currentCustomer]); + + return ( + + {children} + + ); +} + +export function useCustomer() { + const context = useContext(CustomerContext); + + if (context === undefined) { + throw new Error("useCustomer must be used within a CustomerProvider"); + } + + return context; +} \ No newline at end of file