import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { CalendarIcon, ArrowLeft } from 'lucide-react'; import * as z from 'zod'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Calendar } from '@/components/ui/calendar'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { useCategories } from '@/hooks/useCategories'; import { InvoiceUpload } from './InvoiceUpload'; const serviceSchema = z.object({ service_name: z.string().min(1, 'Service name is required'), category_id: z.string().min(1, 'Category is required'), provider: z.string().min(1, 'Provider is required'), vendor_id: z.union([z.string(), z.literal('none')]).optional(), plan_name: z.string().optional(), account_email: z.string().email().optional().or(z.literal('')), dashboard_url: z.string().url().optional().or(z.literal('')), start_date: z.date(), billing_cycle: z.enum(['Monthly', 'Quarterly', 'Semi-Annual', 'Annual', 'Custom_days']), custom_cycle_days: z.number().positive().optional(), amount: z.number().min(0, 'Amount cannot be negative'), currency: z.enum(['INR', 'USD', 'EUR']).default('INR'), exchange_rate: z.number().positive().optional(), payment_method: z.union([ z.enum(['Card', 'UPI', 'NetBanking', 'Bank Transfer', 'PayPal', 'Other']), z.literal('none') ]).optional(), auto_renew: z.boolean().default(false), next_renewal_date: z.date().optional(), next_renewal_amount: z.number().min(0, 'Amount cannot be negative').optional(), reminder_days_before: z.number().positive().default(7), status: z.enum(['Active', 'Paused', 'Cancelled', 'Expired']).default('Active'), importance: z.enum(['Critical', 'Normal', 'Nice-to-have']).default('Normal'), tags: z.string().optional(), notes: z.string().optional(), }).refine((data) => { if (data.billing_cycle === 'Custom_days' && !data.custom_cycle_days) { return false; } return true; }, { message: "Custom cycle days is required when billing cycle is Custom_days", path: ["custom_cycle_days"], }).refine((data) => { if (data.currency !== 'INR' && !data.exchange_rate) { return false; } return true; }, { message: "Exchange rate is required for non-INR currencies", path: ["exchange_rate"], }); type ServiceFormData = z.infer; export function ServiceForm() { const navigate = useNavigate(); const { toast } = useToast(); const queryClient = useQueryClient(); const [invoiceFileUrl, setInvoiceFileUrl] = useState(null); const form = useForm({ resolver: zodResolver(serviceSchema), defaultValues: { currency: 'INR', auto_renew: false, reminder_days_before: 7, status: 'Active', importance: 'Normal', start_date: new Date(), }, }); const billingCycle = form.watch('billing_cycle'); // Fetch categories for the dropdown const { data: categories, isLoading: categoriesLoading, error: categoriesError } = useCategories(); console.log('ServiceForm: Categories state:', { count: categories?.length, loading: categoriesLoading, error: categoriesError }); // Fetch vendors for the dropdown const { data: vendors } = useQuery({ queryKey: ['vendors'], queryFn: async () => { const { data, error } = await supabase .from('vendors') .select('id, name') .order('name'); if (error) throw error; return data || []; }, }); const createServiceMutation = useMutation({ mutationFn: async (data: ServiceFormData) => { const { data: user } = await supabase.auth.getUser(); if (!user.user) throw new Error('Not authenticated'); // Parse tags const tagsArray = data.tags ? data.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0) : []; // Calculate next renewal date if not provided let nextRenewalDate = data.next_renewal_date; if (!nextRenewalDate) { const startDate = new Date(data.start_date); switch (data.billing_cycle) { case 'Monthly': nextRenewalDate = new Date(startDate.setMonth(startDate.getMonth() + 1)); break; case 'Quarterly': nextRenewalDate = new Date(startDate.setMonth(startDate.getMonth() + 3)); break; case 'Semi-Annual': nextRenewalDate = new Date(startDate.setMonth(startDate.getMonth() + 6)); break; case 'Annual': nextRenewalDate = new Date(startDate.setFullYear(startDate.getFullYear() + 1)); break; case 'Custom_days': nextRenewalDate = new Date(startDate.setDate(startDate.getDate() + (data.custom_cycle_days || 30))); break; } } const serviceData = { user_id: user.user.id, service_name: data.service_name, category_id: data.category_id, provider: data.provider, vendor_id: data.vendor_id === 'none' ? null : data.vendor_id || null, plan_name: data.plan_name || null, account_email: data.account_email || null, dashboard_url: data.dashboard_url || null, start_date: data.start_date.toISOString().split('T')[0], billing_cycle: data.billing_cycle, custom_cycle_days: data.custom_cycle_days || null, amount: data.amount, currency: data.currency, payment_method: data.payment_method === 'none' ? null : data.payment_method || null, auto_renew: data.auto_renew, next_renewal_date: nextRenewalDate?.toISOString().split('T')[0] || null, next_renewal_amount: data.next_renewal_amount || null, reminder_days_before: data.reminder_days_before, status: data.status, importance: data.importance, tags: tagsArray, notes: data.notes || null, invoice_file_url: invoiceFileUrl, exchange_rate: data.exchange_rate || null, }; const { error } = await supabase .from('services') .insert([serviceData]); if (error) throw error; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['services'] }); toast({ title: 'Service created', description: 'Your new service has been added successfully.', }); navigate('/services'); }, onError: (error: any) => { toast({ title: 'Error', description: error.message, variant: 'destructive', }); }, }); const onSubmit = (data: ServiceFormData) => { createServiceMutation.mutate(data); }; const currencies = ['INR', 'USD', 'EUR']; const billingCycles = ['Monthly', 'Quarterly', 'Semi-Annual', 'Annual', 'Custom_days']; const paymentMethods = ['Card', 'UPI', 'NetBanking', 'Bank Transfer', 'PayPal', 'Other']; const statuses = ['Active', 'Paused', 'Cancelled', 'Expired']; const importanceLevels = ['Critical', 'Normal', 'Nice-to-have']; return (

Add New Service

Add a new subscription or service to track

{/* Basic Information */} Basic Information ( Service Name * )} /> ( Category * )} /> ( Provider * )} /> ( Vendor (Optional) )} /> ( Plan Name )} /> {/* Contact & Access */} Contact & Access ( Account Email )} /> ( Dashboard URL )} /> ( Importance )} /> ( Status )} /> ( Tags Enter tags separated by commas )} />
{/* Billing Information */} Billing Information ( Start Date * )} /> ( Billing Cycle * )} /> {billingCycle === 'Custom_days' && ( ( Custom Cycle Days * field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} /> )} /> )} ( Amount * field.onChange(e.target.value ? parseFloat(e.target.value) : 0)} /> )} /> ( Currency * )} /> ( Exchange Rate to USD {form.watch('currency') !== 'INR' ? '*' : '(Optional)'} field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)} /> {form.watch('currency') !== 'INR' ? `Enter the current exchange rate: 1 ${form.watch('currency')} = X USD` : 'Optional: Track exchange rate for historical analysis'} )} /> ( Payment Method )} /> {/* Renewal Settings */} Renewal Settings ( Next Renewal Date date < new Date()} initialFocus className={cn("p-3 pointer-events-auto")} /> Leave empty to auto-calculate based on start date and billing cycle )} /> ( Next Renewal Amount (optional) field.onChange(e.target.value ? Number(e.target.value) : undefined)} /> If the upcoming invoice amount differs from the current plan. )} /> ( Reminder Days Before field.onChange(e.target.value ? parseInt(e.target.value) : 7)} /> How many days before renewal to send reminders )} /> (
Auto Renewal Enable automatic renewal for this service
)} />
{/* Notes & Invoice */} Additional Information ( Notes