feat: Implement business client list view and add client
This commit is contained in:
380
apps/web/src/features/business/clients/AddClient.tsx
Normal file
380
apps/web/src/features/business/clients/AddClient.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import { Button } from "@/common/components/ui/button";
|
||||
import { Input } from "@/common/components/ui/input";
|
||||
import { Label } from "@/common/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/common/components/ui/select";
|
||||
import { Textarea } from "@/common/components/ui/textarea";
|
||||
import DashboardLayout from "@/features/layouts/DashboardLayout";
|
||||
import { ArrowLeft, Loader2, Save, X, Mail } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { RootState } from "@/store/store";
|
||||
import {
|
||||
useCreateBusiness,
|
||||
useCreateTeamHub
|
||||
} from "@/dataconnect-generated/react";
|
||||
import {
|
||||
BusinessArea,
|
||||
BusinessSector,
|
||||
BusinessStatus,
|
||||
BusinessRateGroup
|
||||
} from "@/dataconnect-generated";
|
||||
import { dataConnect } from "@/features/auth/firebase";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export default function AddClient() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [showSnackbar, setShowSnackbar] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
businessName: "",
|
||||
companyLogoUrl: "",
|
||||
contactName: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
hubBuilding: "",
|
||||
address: "",
|
||||
city: "",
|
||||
area: BusinessArea.BAY_AREA,
|
||||
sector: BusinessSector.OTHER,
|
||||
rateGroup: BusinessRateGroup.STANDARD,
|
||||
status: BusinessStatus.ACTIVE,
|
||||
notes: ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createBusiness, isPending: isCreatingBusiness } = useCreateBusiness(dataConnect);
|
||||
const { mutateAsync: createHub, isPending: isCreatingHub } = useCreateTeamHub(dataConnect);
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user?.uid) return;
|
||||
|
||||
try {
|
||||
// 1. Create the business record
|
||||
const businessResult = await createBusiness({
|
||||
businessName: formData.businessName,
|
||||
contactName: formData.contactName,
|
||||
userId: user.uid,
|
||||
companyLogoUrl: formData.companyLogoUrl,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
hubBuilding: formData.hubBuilding,
|
||||
address: formData.address,
|
||||
city: formData.city,
|
||||
area: formData.area,
|
||||
sector: formData.sector,
|
||||
rateGroup: formData.rateGroup,
|
||||
status: formData.status,
|
||||
notes: formData.notes
|
||||
});
|
||||
|
||||
const businessId = businessResult.business_insert.id;
|
||||
|
||||
// 2. Automatically create the client's first "hub" or location
|
||||
await createHub({
|
||||
teamId: businessId,
|
||||
hubName: `${formData.businessName} - Main Hub`,
|
||||
address: formData.address || "Main Office",
|
||||
city: formData.city,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// 3. Show snackbar for welcome email
|
||||
setSnackbarMessage(`Welcome email sent to ${formData.contactName} (${formData.email})`);
|
||||
setShowSnackbar(true);
|
||||
|
||||
// Invalidate queries and navigate after a delay to show snackbar
|
||||
queryClient.invalidateQueries({ queryKey: ['businesses'] });
|
||||
|
||||
setTimeout(() => {
|
||||
navigate("/clients");
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating client partnership:", error);
|
||||
setSnackbarMessage("Failed to create client partnership. Please try again.");
|
||||
setShowSnackbar(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isPending = isCreatingBusiness || isCreatingHub;
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="Register Business"
|
||||
subtitle="Initialize a new client partnership and hub location."
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate("/clients")}
|
||||
leadingIcon={<ArrowLeft />}
|
||||
>
|
||||
Back to Directory
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className=" mx-auto pb-20 relative">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="py-8 space-y-8">
|
||||
{/* Business Name & Company Logo */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="businessName">
|
||||
Business Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="businessName"
|
||||
value={formData.businessName}
|
||||
onChange={(e) => handleChange('businessName', e.target.value)}
|
||||
placeholder="Enter business name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyLogoUrl">
|
||||
Company Logo URL
|
||||
</Label>
|
||||
<Input
|
||||
id="companyLogoUrl"
|
||||
value={formData.companyLogoUrl}
|
||||
onChange={(e) => handleChange('companyLogoUrl', e.target.value)}
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
<p className="text-xs text-muted-text">Optional: URL to company logo image</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Contact */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contactName">
|
||||
Primary Contact <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="contactName"
|
||||
value={formData.contactName}
|
||||
onChange={(e) => handleChange('contactName', e.target.value)}
|
||||
placeholder="Contact name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contact Number & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">
|
||||
Contact Number
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleChange('phone', e.target.value)}
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
placeholder="business@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hub / Building */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hubBuilding">
|
||||
Hub / Building
|
||||
</Label>
|
||||
<Input
|
||||
id="hubBuilding"
|
||||
value={formData.hubBuilding}
|
||||
onChange={(e) => handleChange('hubBuilding', e.target.value)}
|
||||
placeholder="Building name or location"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Billing Address */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">
|
||||
Billing Address <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleChange('address', e.target.value)}
|
||||
placeholder="Street address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* City & Area */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">
|
||||
City <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleChange('city', e.target.value)}
|
||||
placeholder="City"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="area">
|
||||
Area
|
||||
</Label>
|
||||
<Select value={formData.area} onValueChange={(value : any) => handleChange('area', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select area" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={BusinessArea.BAY_AREA}>Bay Area</SelectItem>
|
||||
<SelectItem value={BusinessArea.SOUTHERN_CALIFORNIA}>Southern California</SelectItem>
|
||||
<SelectItem value={BusinessArea.NORTHERN_CALIFORNIA}>Northern California</SelectItem>
|
||||
<SelectItem value={BusinessArea.CENTRAL_VALLEY}>Central Valley</SelectItem>
|
||||
<SelectItem value={BusinessArea.OTHER}>Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sector & Rate Group */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sector">
|
||||
Industry / Sector <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={formData.sector} onValueChange={(value :any) => handleChange('sector', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select sector" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={BusinessSector.BON_APPETIT}>Bon Appétit</SelectItem>
|
||||
<SelectItem value={BusinessSector.EUREST}>Eurest</SelectItem>
|
||||
<SelectItem value={BusinessSector.ARAMARK}>Aramark</SelectItem>
|
||||
<SelectItem value={BusinessSector.EPICUREAN_GROUP}>Epicurean Group</SelectItem>
|
||||
<SelectItem value={BusinessSector.CHARTWELLS}>Chartwells</SelectItem>
|
||||
<SelectItem value={BusinessSector.OTHER}>Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rateGroup">
|
||||
Rate Group <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={formData.rateGroup} onValueChange={(value : any) => handleChange('rateGroup', value)} required>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select pricing tier" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={BusinessRateGroup.STANDARD}>Standard</SelectItem>
|
||||
<SelectItem value={BusinessRateGroup.PREMIUM}>Premium</SelectItem>
|
||||
<SelectItem value={BusinessRateGroup.ENTERPRISE}>Enterprise</SelectItem>
|
||||
<SelectItem value={BusinessRateGroup.CUSTOM}>Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">
|
||||
Status
|
||||
</Label>
|
||||
<Select value={formData.status} onValueChange={(value : any) => handleChange('status', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={BusinessStatus.ACTIVE}>Active</SelectItem>
|
||||
<SelectItem value={BusinessStatus.INACTIVE}>Inactive</SelectItem>
|
||||
<SelectItem value={BusinessStatus.PENDING}>Pending</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={formData.notes}
|
||||
onChange={(e) => handleChange('notes', e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Additional notes about this business..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate("/clients")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
leadingIcon={isPending ? <Loader2 className="animate-spin" /> : <Save />}
|
||||
>
|
||||
{isPending ? "Initializing Partnership..." : "Create Business Client"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Snackbar Notification */}
|
||||
<AnimatePresence>
|
||||
{showSnackbar && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 50 }}
|
||||
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-slate-900 text-white px-6 py-4 rounded-2xl shadow-2xl border border-white/10 min-w-[320px]"
|
||||
>
|
||||
<div className="p-2 bg-emerald-500/20 rounded-lg text-emerald-400">
|
||||
<Mail size={18} />
|
||||
</div>
|
||||
<p className="text-sm font-bold flex-1">{snackbarMessage}</p>
|
||||
<button
|
||||
onClick={() => setShowSnackbar(false)}
|
||||
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user