import type { EventPlace } from "@/entities/place";
import { pluralTickets } from "@/shared/lib/pluralDays";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { useState } from "react";
import type { Control } from "react-hook-form";
import { useFieldArray, useWatch } from "react-hook-form";
import type { EventFormOutput } from "../model/types";
interface CategorySectionProps {
control: Control<EventFormOutput>;
selectedPlace?: EventPlace;
}
export const CategorySection: React.FC<CategorySectionProps> = ({
control,
selectedPlace,
}) => {
const { fields, append, remove, replace } = useFieldArray({
control,
name: "categories",
});
const categories = useWatch({
control,
name: "categories",
defaultValue: [],
});
const [isAdding, setIsAdding] = useState(false);
const [error, setError] = useState<string | null>(null);
const [newCategory, setNewCategory] = useState({
name: "",
discount: "",
quota: "",
});
const maxTickets = selectedPlace?.maxTickets ?? Number.MAX_SAFE_INTEGER;
const minTickets = selectedPlace?.minTickets ?? 1;
// Calculate total quota used by all categories.
const totalQuotaUsed =
categories?.reduce((sum, cat) => {
return sum + cat.quota;
}, 0) ?? 0;
// Calculate remaining tickets for categories.
const remainingTickets = Math.max(0, maxTickets - totalQuotaUsed);
const isBelowMinTickets = totalQuotaUsed < minTickets && fields.length > 0;
function findQuotaDonorIndex(requiredExtraQuota: number): number {
if (requiredExtraQuota <= 0) return -1;
let donorIndex = -1;
let donorQuota = 0;
for (let i = 0; i < categories.length; i++) {
const category = categories[i];
if (!category) continue;
// Keep at least 1 ticket in donor category after reallocation.
if (category.quota - requiredExtraQuota < 1) continue;
if (category.quota > donorQuota) {
donorQuota = category.quota;
donorIndex = i;
}
}
return donorIndex;
}
function validateCategory(
quotaValue: number,
requiredExtraQuota: number,
): string | null {
const discountValue = Number(newCategory.discount);
// Validate discount range
if (
newCategory.discount !== "" &&
(discountValue < 0 || discountValue > 100)
) {
return "Discount must be between 0 and 100";
}
// Validate quota
if (quotaValue < 1) {
return "Quota must be at least 1 ticket";
}
if (quotaValue > maxTickets) {
return `Quota cannot exceed maximum event capacity (${pluralTickets(maxTickets)})`;
}
if (requiredExtraQuota > 0) {
const donorIndex = findQuotaDonorIndex(requiredExtraQuota);
if (donorIndex === -1) {
return `Cannot add ${pluralTickets(quotaValue)}. Only ${pluralTickets(remainingTickets)} free and no existing category can donate ${pluralTickets(requiredExtraQuota)}.`;
}
}
return null;
}
function handleAdd() {
if (!newCategory.name) return;
const quotaValue = Number(newCategory.quota);
const requiredExtraQuota = Math.max(0, quotaValue - remainingTickets);
const validationError = validateCategory(quotaValue, requiredExtraQuota);
if (validationError) {
setError(validationError);
return;
}
if (requiredExtraQuota > 0) {
const donorIndex = findQuotaDonorIndex(requiredExtraQuota);
if (donorIndex !== -1) {
const donor = categories[donorIndex];
if (donor) {
const next = [...categories];
next[donorIndex] = {
...donor,
quota: donor.quota - requiredExtraQuota,
};
replace(next);
}
}
}
const discountValue =
newCategory.discount === "" ? 0 : Number(newCategory.discount);
append({
id: crypto.randomUUID(),
name: newCategory.name,
discount: discountValue,
quota: quotaValue,
});
setNewCategory({ name: "", discount: "", quota: "" });
setError(null);
setIsAdding(false);
}
function handleRemove(index: number) {
remove(index);
setError(null);
}
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Ticket Categories (Required)</h3>
<p className="text-sm text-muted-foreground">
Set up ticket categories for your event. Each category must have a
positive quota. The total quota cannot exceed place capacity.
</p>
{fields.length === 0 && (
<div className="text-sm text-red-500 bg-red-500/10 p-3 rounded">
At least one category is required. Please add a category to continue.
</div>
)}
{fields.length > 0 && (
<div className="space-y-2">
{fields.map((field, index) => (
<div
key={field.id}
className="flex items-center justify-between p-3 bg-popover border border-input"
>
<div>
<span className="font-medium">{field.name}</span>
<span className="text-sm text-muted-foreground ml-2">
({Number(field.discount).toFixed(2)}% off)
</span>
<span className="text-sm text-muted-foreground ml-2">
{pluralTickets(field.quota)}
</span>
</div>
<button
className="cursor-pointer p-1"
type="button"
onClick={() => handleRemove(index)}
>
<img
className="h-5 w-5"
src="/icons/staff bin_2.svg"
alt="remove category"
/>
</button>
</div>
))}
</div>
)}
{isBelowMinTickets && (
<div className="text-sm text-amber-500 bg-amber-500/10 p-3 rounded">
Total quota must be at least {minTickets}. Increase category quotas.
</div>
)}
{remainingTickets <= 0 && (
<p className="text-sm text-amber-500">
All {pluralTickets(maxTickets)} are allocated. Adding a new category
will reallocate quota from an existing category.
</p>
)}
{!isAdding ? (
<Button
filling="outline"
className="w-full"
onClick={() => setIsAdding(true)}
>
Add Category
{remainingTickets > 0 && (
<span className="ml-2 text-xs text-muted-foreground">
({pluralTickets(remainingTickets)} available)
</span>
)}
</Button>
) : (
<div className="space-y-3 p-4 bg-popover border border-input">
{error && (
<div className="text-sm text-red-500 bg-red-500/10 p-2 rounded">
{error}
</div>
)}
<div className="space-y-2">
<label htmlFor="cat-name" className="text-sm">
Category Name
</label>
<Input
id="cat-name"
value={newCategory.name}
onChange={(e) =>
setNewCategory({ ...newCategory, name: e.target.value })
}
placeholder="e.g., Early Bird, Students"
/>
</div>
<div className="space-y-2">
<label htmlFor="cat-discount" className="text-sm">
Discount (%) - 0 to 100
</label>
<Input
id="cat-discount"
type="number"
min="0"
max="100"
step="0.01"
value={newCategory.discount}
onChange={(e) =>
setNewCategory({ ...newCategory, discount: e.target.value })
}
placeholder="0-100"
/>
</div>
<div className="space-y-2">
<label htmlFor="cat-quota" className="text-sm">
Ticket Limit (1 to {maxTickets})
</label>
<Input
id="cat-quota"
type="number"
min="1"
max={maxTickets}
value={newCategory.quota}
onChange={(e) => {
setNewCategory({ ...newCategory, quota: e.target.value });
setError(null);
}}
placeholder={`1-${maxTickets}`}
/>
</div>
<div className="flex gap-2">
<Button
filling="outline"
className="flex-1"
onClick={() => {
setIsAdding(false);
setNewCategory({
name: "",
discount: "",
quota: "",
});
setError(null);
}}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={handleAdd}
disabled={!newCategory.name}
>
Add
</Button>
</div>
</div>
)}
</div>
);
};