Files
accounted/components/campaigns/ExclusivityForm.tsx
T

382 lines
12 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import type { Exclusivity, CreateExclusivityInput, ExclusivityConflict } from '@/types'
import { AlertTriangle, X, Plus } from 'lucide-react'
interface ExclusivityFormProps {
open: boolean
onOpenChange: (open: boolean) => void
campaignId: string
initialData?: Partial<Exclusivity>
onSuccess?: (exclusivity: Exclusivity) => void
}
// Common category suggestions
const CATEGORY_SUGGESTIONS = [
'Skönhet', 'Hudvård', 'Smink', 'Mode', 'Kläder', 'Accessoarer',
'Mat', 'Dryck', 'Kaffe', 'Energidryck', 'Snacks',
'Teknik', 'Mobil', 'Gaming', 'Elektronik',
'Träning', 'Gym', 'Kosttillskott', 'Sport',
'Resor', 'Hotell', 'Flyg',
'Bank', 'Finans', 'Försäkring',
'Bil', 'Bilar', 'Transport'
]
export function ExclusivityForm({
open,
onOpenChange,
campaignId,
initialData,
onSuccess
}: ExclusivityFormProps) {
const { toast } = useToast()
const [isLoading, setIsLoading] = useState(false)
const [isCheckingConflicts, setIsCheckingConflicts] = useState(false)
const [conflicts, setConflicts] = useState<ExclusivityConflict[]>([])
const [newCategory, setNewCategory] = useState('')
const [newBrand, setNewBrand] = useState('')
const [formData, setFormData] = useState<Omit<CreateExclusivityInput, 'campaign_id'>>({
categories: [],
excluded_brands: [],
start_date: '',
end_date: '',
notes: '',
})
useEffect(() => {
if (open) {
setFormData({
categories: initialData?.categories || [],
excluded_brands: initialData?.excluded_brands || [],
start_date: initialData?.start_date || '',
end_date: initialData?.end_date || '',
notes: initialData?.notes || '',
})
setConflicts([])
setNewCategory('')
setNewBrand('')
}
}, [open, initialData])
// Check for conflicts when dates or categories change
const checkConflicts = async () => {
if (!formData.categories.length || !formData.start_date || !formData.end_date) {
setConflicts([])
return
}
setIsCheckingConflicts(true)
try {
const params = new URLSearchParams({
categories: formData.categories.join(','),
start_date: formData.start_date,
end_date: formData.end_date,
exclude_campaign_id: campaignId,
})
const response = await fetch(`/api/exclusivities/conflicts?${params}`)
if (response.ok) {
const data = await response.json()
setConflicts(data.conflicts || [])
}
} catch (error) {
console.error('Failed to check conflicts:', error)
} finally {
setIsCheckingConflicts(false)
}
}
useEffect(() => {
const timer = setTimeout(checkConflicts, 500)
return () => clearTimeout(timer)
}, [formData.categories, formData.start_date, formData.end_date])
const addCategory = (category: string) => {
const trimmed = category.trim()
if (trimmed && !formData.categories.includes(trimmed)) {
setFormData({
...formData,
categories: [...formData.categories, trimmed]
})
}
setNewCategory('')
}
const removeCategory = (category: string) => {
setFormData({
...formData,
categories: formData.categories.filter(c => c !== category)
})
}
const addBrand = (brand: string) => {
const trimmed = brand.trim()
if (trimmed && !formData.excluded_brands?.includes(trimmed)) {
setFormData({
...formData,
excluded_brands: [...(formData.excluded_brands || []), trimmed]
})
}
setNewBrand('')
}
const removeBrand = (brand: string) => {
setFormData({
...formData,
excluded_brands: formData.excluded_brands?.filter(b => b !== brand) || []
})
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (formData.categories.length === 0) {
toast({ title: 'Minst en kategori krävs', variant: 'destructive' })
return
}
if (!formData.start_date || !formData.end_date) {
toast({ title: 'Start- och slutdatum krävs', variant: 'destructive' })
return
}
setIsLoading(true)
try {
const url = initialData?.id
? `/api/exclusivities/${initialData.id}`
: `/api/campaigns/${campaignId}/exclusivities`
const method = initialData?.id ? 'PATCH' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to save exclusivity')
}
const result = await response.json()
// Show warning if there are conflicts
if (result.warning) {
toast({
title: 'Exklusivitet sparad med varning',
description: result.warning,
variant: 'default',
})
} else {
toast({
title: initialData?.id ? 'Exklusivitet uppdaterad' : 'Exklusivitet tillagd',
})
}
onOpenChange(false)
onSuccess?.(result.data)
} catch (error) {
toast({
title: 'Fel',
description: error instanceof Error ? error.message : 'Något gick fel',
variant: 'destructive',
})
} finally {
setIsLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{initialData?.id ? 'Redigera exklusivitet' : 'Lägg till exklusivitet'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Categories */}
<div>
<Label>Kategorier *</Label>
<div className="flex flex-wrap gap-2 mt-2 mb-2">
{formData.categories.map(category => (
<Badge key={category} variant="secondary" className="gap-1">
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addCategory(newCategory)
}
}}
placeholder="Lägg till kategori..."
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => addCategory(newCategory)}
>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex flex-wrap gap-1 mt-2">
{CATEGORY_SUGGESTIONS
.filter(s => !formData.categories.includes(s))
.slice(0, 8)
.map(suggestion => (
<button
key={suggestion}
type="button"
onClick={() => addCategory(suggestion)}
className="text-xs text-muted-foreground hover:text-foreground px-2 py-0.5 rounded border hover:border-primary"
>
+ {suggestion}
</button>
))}
</div>
</div>
{/* Excluded brands */}
<div>
<Label>Specifika varumärken som exkluderas (valfritt)</Label>
<div className="flex flex-wrap gap-2 mt-2 mb-2">
{formData.excluded_brands?.map(brand => (
<Badge key={brand} variant="outline" className="gap-1">
{brand}
<button
type="button"
onClick={() => removeBrand(brand)}
className="hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
value={newBrand}
onChange={(e) => setNewBrand(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addBrand(newBrand)
}
}}
placeholder="Lägg till varumärke..."
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => addBrand(newBrand)}
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
{/* Date range */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="start_date">Startdatum *</Label>
<Input
id="start_date"
type="date"
value={formData.start_date}
onChange={(e) => setFormData({ ...formData, start_date: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="end_date">Slutdatum *</Label>
<Input
id="end_date"
type="date"
value={formData.end_date}
onChange={(e) => setFormData({ ...formData, end_date: e.target.value })}
required
/>
</div>
</div>
{/* Conflict warnings */}
{conflicts.length > 0 && (
<div className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-start gap-2">
<AlertTriangle className="h-5 w-5 text-orange-600 flex-shrink-0" />
<div>
<p className="font-medium text-orange-800">
{conflicts.length} överlappande exklusivitet(er)
</p>
<ul className="text-sm text-orange-700 mt-1 space-y-1">
{conflicts.map((conflict, i) => (
<li key={i}>
<strong>{conflict.conflictingCampaign?.name || 'Okänd kampanj'}</strong>
: {conflict.overlappingCategories.join(', ')}
{' '}({conflict.overlapStart} - {conflict.overlapEnd})
</li>
))}
</ul>
</div>
</div>
</div>
)}
{/* Notes */}
<div>
<Label htmlFor="notes">Anteckningar</Label>
<Textarea
id="notes"
value={formData.notes || ''}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="Detaljer om exklusiviteten..."
rows={2}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Avbryt
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Sparar...' : initialData?.id ? 'Spara' : 'Lägg till'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}