mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-07-26 11:38:13 +00:00
Add customer management UI components
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/7e31146a-cd1d-4738-8630-47e80ea2b8a0.jpg
This commit is contained in:
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Customers</CardTitle>
|
||||
<CardDescription>
|
||||
You don't have any customers yet.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<p className="text-muted-foreground">
|
||||
Create a customer in the settings page.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Customers</CardTitle>
|
||||
<CardDescription>
|
||||
Select a customer to manage their domains and DNS records
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{customers.map((customer) => (
|
||||
<div
|
||||
key={customer.id}
|
||||
className={`p-3 rounded-md cursor-pointer transition-colors border ${
|
||||
currentCustomer?.id === customer.id ? "bg-accent border-primary/50" : "hover:bg-muted/50 border-transparent"
|
||||
}`}
|
||||
onClick={() => setCurrentCustomer(customer)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="font-medium flex items-center">
|
||||
<Server className="h-4 w-4 mr-2 text-primary" />
|
||||
{customer.name}
|
||||
{!customer.isActive && (
|
||||
<span className="ml-2 text-xs bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300 rounded-full px-2 py-0.5">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground flex flex-wrap gap-x-3">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
<span>3 Members</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
<span>Created {format(new Date(customer.createdAt), 'MMM d, yyyy')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium rounded-md bg-secondary text-secondary-foreground opacity-70 animate-pulse">
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-muted rounded-full mr-2"></span>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentCustomer || customers.length === 0) {
|
||||
return (
|
||||
<div className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium rounded-md bg-secondary text-secondary-foreground">
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-warning rounded-full mr-2"></span>
|
||||
<span>No Customer</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium rounded-md hover:bg-accent">
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-success rounded-full mr-2"></span>
|
||||
<span>{currentCustomer.name}</span>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
{customers.map((customer) => (
|
||||
<DropdownMenuItem
|
||||
key={customer.id}
|
||||
onClick={() => setCurrentCustomer(customer)}
|
||||
className={customer.id === currentCustomer.id ? "bg-muted" : ""}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className={`w-2 h-2 rounded-full mr-2 ${customer.isActive ? "bg-success" : "bg-warning"}`}></span>
|
||||
<span>{customer.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -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<Customer, Error, InsertCustomer>;
|
||||
updateCustomerMutation: UseMutationResult<Customer, Error, { id: string; data: Partial<InsertCustomer> }>;
|
||||
deleteCustomerMutation: UseMutationResult<boolean, Error, string>;
|
||||
domainsLoading: boolean;
|
||||
domainsByCustomer: Record<string, Domain[]>;
|
||||
};
|
||||
|
||||
const CustomerContext = createContext<CustomerContextType | undefined>(undefined);
|
||||
|
||||
export function CustomerProvider({ children }: { children: ReactNode }) {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [currentCustomer, setCurrentCustomer] = useState<Customer | null>(null);
|
||||
const [domainsByCustomer, setDomainsByCustomer] = useState<Record<string, Domain[]>>({});
|
||||
|
||||
// Get customers
|
||||
const { data: customers = [], isLoading } = useQuery<Customer[]>({
|
||||
queryKey: ["/api/customers"],
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
// Load domains for current customer
|
||||
const { isLoading: domainsLoading } = useQuery<Domain[]>({
|
||||
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<InsertCustomer> }) => {
|
||||
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 (
|
||||
<CustomerContext.Provider
|
||||
value={{
|
||||
customers,
|
||||
currentCustomer,
|
||||
setCurrentCustomer,
|
||||
isLoading,
|
||||
domainsLoading,
|
||||
domainsByCustomer,
|
||||
createCustomerMutation,
|
||||
updateCustomerMutation,
|
||||
deleteCustomerMutation
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CustomerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCustomer() {
|
||||
const context = useContext(CustomerContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useCustomer must be used within a CustomerProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user