All files / components/invitations CreateInvitationDialog.tsx

61.29% Statements 19/31
75% Branches 9/12
57.14% Functions 4/7
61.29% Lines 19/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                                                            65x 65x 65x 65x 65x 65x   65x 21x 21x 21x 21x 21x 21x 21x           21x     21x     65x                                           65x                                                                                                             23x                          
 
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { AppDialog } from '@/components/common/AppDialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from '@/components/ui/select';
import { createInvitation, type InvitationCreateRequest } from '@/services/invitations.service';
import { getAccounts, type AccountListItem } from '@/services/accounts.service';
import { handleApiError } from '@/lib/api';
import { useToast } from "@/hooks/use-toast";
 
interface CreateInvitationDialogProps {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    onSuccess: () => void;
}
 
export function CreateInvitationDialog({
    open,
    onOpenChange,
    onSuccess,
}: CreateInvitationDialogProps) {
    const { toast } = useToast();
    const [isLoading, setIsLoading] = useState(false);
    const [accounts, setAccounts] = useState<AccountListItem[]>([]);
    const [loadingAccounts, setLoadingAccounts] = useState(false);
    const [showAccountSelect, setShowAccountSelect] = useState(false);
    const { register, handleSubmit, reset, getValues, formState: { errors } } = useForm<InvitationCreateRequest>();
 
    useEffect(() => {
        const loadAccounts = async () => {
            try {
                setLoadingAccounts(true);
                const response = await getAccounts({ page_size: 100 });
                Eif (response.items && response.items.length > 0) {
                    setAccounts(response.items);
                    setShowAccountSelect(true);
                }
            } catch (error) {
                // Ignore error - likely not superadmin, so just don't show account selector
                console.debug("Failed to load accounts for invitation selector", error);
            } finally {
                setLoadingAccounts(false);
            }
        };
        loadAccounts();
    }, []);
 
    const onSubmit = async (data: InvitationCreateRequest) => {
        setIsLoading(true);
        try {
            await createInvitation(data);
            toast({
                title: "Invitation sent",
                description: `Invitation sent to ${data.email}`,
            });
            reset();
            onSuccess();
            onOpenChange(false);
        } catch (err) {
            toast({
                variant: "destructive",
                title: "Error",
                description: handleApiError(err),
            });
        } finally {
            setIsLoading(false);
        }
    };
 
    return (
        <AppDialog
            open={open}
            onOpenChange={onOpenChange}
            title="Invite User"
            description="Send an invitation email to a new user. They will receive a link to set their password."
            footer={
                <>
                    <Button
                        type="button"
                        variant="outline"
                        onClick={() => onOpenChange(false)}
                        disabled={isLoading}
                    >
                        Cancel
                    </Button>
                    <Button type="submit" form="create-invitation-form" disabled={isLoading}>
                        {isLoading ? 'Sending...' : 'Send Invitation'}
                    </Button>
                </>
            }
        >
            <form id="create-invitation-form" onSubmit={handleSubmit(onSubmit)} className="space-y-4">
                <div className="space-y-2">
                    <Label htmlFor="email">Email address</Label>
                    <Input
                        id="email"
                        type="email"
                        placeholder="user@example.com"
                        disabled={isLoading}
                        {...register('email', {
                            required: 'Email is required',
                            pattern: {
                                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                                message: "Invalid email address"
                            }
                        })}
                    />
                    {errors.email && (
                        <p className="text-sm text-destructive">{errors.email.message}</p>
                    )}
                </div>
 
                {showAccountSelect && (
                    <div className="space-y-2">
                        <Label htmlFor="account">Account (Optional)</Label>
                        <Select
                            onValueChange={(value) => reset({ ...getValues(), account_id: value })}
                            disabled={isLoading || loadingAccounts}
                        >
                            <SelectTrigger>
                                <SelectValue placeholder="Select an account" />
                            </SelectTrigger>
                            <SelectContent>
                                {accounts.map((account) => (
                                    <SelectItem key={account.id} value={account.id}>
                                        {account.name} ({account.account_code})
                                    </SelectItem>
                                ))}
                            </SelectContent>
                        </Select>
                        <p className="text-xs text-muted-foreground">Leave empty to invite to your current account.</p>
                    </div>
                )}
            </form>
        </AppDialog>
    );
}