turash/bugulma/frontend/pages/OrganizationEditPage.tsx
2025-12-15 10:06:41 +01:00

381 lines
14 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeft, CheckCircle } from 'lucide-react';
import { MainLayout } from '@/components/layout/MainLayout.tsx';
import PageHeader from '@/components/layout/PageHeader.tsx';
import Button from '@/components/ui/Button.tsx';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
import Input from '@/components/ui/Input.tsx';
import { Label, FormField } from '@/components/ui';
import { Container, Flex, Stack } from '@/components/ui/layout';
import Select from '@/components/ui/Select.tsx';
import Spinner from '@/components/ui/Spinner.tsx';
import Textarea from '@/components/ui/Textarea.tsx';
import { useDynamicSectors } from '@/hooks/useDynamicSectors.ts';
import { useAuth } from '@/contexts/AuthContext.tsx';
import { useCreateOrganization, useOrganization } from '@/hooks/api/useOrganizationsAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useNavigation } from '@/hooks/useNavigation.tsx';
import { isValidEmail, sanitizeInput, validateInput } from '@/lib/api-client.ts';
import { getTranslatedSectorName } from '@/lib/sector-mapper.ts';
import { getOrganizationSubtypeLabel } from '@/schemas/organizationSubtype.ts';
const OrganizationEditPage = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
const { user, isAuthenticated } = useAuth();
const isEditing = Boolean(id);
const pageTitle = isEditing ? t('organizationEdit.editTitle') : t('organizationEdit.createTitle');
const pageSubtitle = isEditing
? t('organizationEdit.editSubtitle')
: t('organizationEdit.createSubtitle');
// Data fetching
const {
data: existingOrganization,
isLoading: isLoadingOrg,
error: orgError,
} = useOrganization(id || '');
const { sectors: availableSectors } = useDynamicSectors(50); // Get all sectors for editing
// Mutations
const createOrgMutation = useCreateOrganization();
// Form state
const [formData, setFormData] = useState({
name: '',
sector: '',
description: '',
subtype: '',
website: '',
address: '',
});
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Redirect to login if not authenticated
useEffect(() => {
if (!isAuthenticated) {
navigate('/login');
}
}, [isAuthenticated, navigate]);
// Initialize form with existing data when editing
useEffect(() => {
if (isEditing && existingOrganization) {
setFormData({
name: existingOrganization.Name || '',
sector: existingOrganization.Sector || '',
description: existingOrganization.Description || '',
subtype: existingOrganization.Subtype || '',
website: existingOrganization.Website || '',
address: existingOrganization.Address || '',
});
}
}, [isEditing, existingOrganization]);
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
try {
setError(null);
setIsSubmitting(true);
// Input validation and sanitization
const sanitizedName = sanitizeInput(formData.name.trim());
const sanitizedDescription = sanitizeInput(formData.description.trim());
const sanitizedWebsite = formData.website.trim();
const sanitizedAddress = sanitizeInput(formData.address.trim());
// Validate organization name
const nameValidation = validateInput(sanitizedName, {
minLength: 2,
maxLength: 100,
allowSpecialChars: true,
allowNumbers: true,
});
if (!nameValidation.isValid) {
throw new Error(`Organization name: ${nameValidation.error}`);
}
// Validate sector selection
if (!formData.sector) {
throw new Error('Please select an organization sector');
}
// Validate description (optional but if provided, check length)
if (sanitizedDescription && sanitizedDescription.length > 0) {
const descValidation = validateInput(sanitizedDescription, {
minLength: 0,
maxLength: 500,
allowSpecialChars: true,
allowNumbers: true,
});
if (!descValidation.isValid) {
throw new Error(`Description: ${descValidation.error}`);
}
}
// Validate website URL if provided
if (
sanitizedWebsite &&
!isValidEmail(sanitizedWebsite) &&
!sanitizedWebsite.startsWith('http')
) {
// If it looks like an email, validate as email
if (sanitizedWebsite.includes('@')) {
if (!isValidEmail(sanitizedWebsite)) {
throw new Error('Please enter a valid email address or website URL');
}
} else {
throw new Error('Please enter a valid website URL (starting with http:// or https://)');
}
}
// Validate address if provided
if (sanitizedAddress && sanitizedAddress.length > 0) {
const addressValidation = validateInput(sanitizedAddress, {
minLength: 0,
maxLength: 200,
allowSpecialChars: true,
allowNumbers: true,
});
if (!addressValidation.isValid) {
throw new Error(`Address: ${addressValidation.error}`);
}
}
// Create organization with sanitized data
const orgPayload = {
name: sanitizedName,
sector: formData.sector,
description: sanitizedDescription,
subtype: formData.subtype || 'commercial',
website: sanitizedWebsite,
address: sanitizedAddress,
};
const newOrg = await createOrgMutation.mutateAsync(orgPayload);
// Navigate to the organization page
navigate(`/organization/${newOrg.ID}`);
} catch (err) {
console.error('Error saving organization:', err);
setError(err instanceof Error ? err.message : 'Failed to save organization');
} finally {
setIsSubmitting(false);
}
};
const handleCancel = () => {
if (isEditing) {
navigate(`/organization/${id}`);
} else {
navigate('/organizations');
}
};
// Sector options
const sectorOptions = availableSectors.map((sector) => ({
value: sector.backendName,
label: t(sector.nameKey),
}));
// Subtype options (simplified)
const subtypeOptions = [
{ value: 'commercial', label: getOrganizationSubtypeLabel('commercial') },
{ value: 'government', label: getOrganizationSubtypeLabel('government') },
{ value: 'other', label: getOrganizationSubtypeLabel('other') },
];
if (isLoadingOrg && isEditing) {
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<div className="flex items-center justify-center min-h-96">
<div className="text-center">
<Spinner className="h-8 w-8 mx-auto mb-4" />
<p className="text-muted-foreground">{t('organizationEdit.loading')}</p>
</div>
</div>
</Container>
</MainLayout>
);
}
if (orgError && isEditing) {
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('organizationEdit.errorTitle')}
subtitle={t('organizationEdit.errorSubtitle')}
onBack={handleBackNavigation}
/>
<Card>
<CardContent className="py-12">
<div className="text-center">
<p className="text-destructive mb-4">{orgError.message}</p>
<Button onClick={handleBackNavigation}>
<ArrowLeft className="h-4 mr-2 text-current w-4" />
{t('common.back')}
</Button>
</div>
</CardContent>
</Card>
</Container>
</MainLayout>
);
}
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader title={pageTitle} subtitle={pageSubtitle} onBack={handleBackNavigation} />
<Stack spacing="2xl">
{/* Action Bar */}
<Flex align="center" justify="between" className="flex-wrap gap-4">
<Button variant="outline" onClick={handleCancel}>
<ArrowLeft className="h-4 mr-2 text-current w-4" />
{t('common.cancel')}
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? (
<>
<Spinner className="h-4 w-4 mr-2" />
{t('organizationEdit.saving')}
</>
) : (
<>
<CheckCircle className="h-4 mr-2 text-current w-4" />
{isEditing
? t('organizationEdit.saveChanges')
: t('organizationEdit.createOrganization')}
</>
)}
</Button>
</Flex>
{/* Error Message */}
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<p className="text-destructive">{error}</p>
</CardContent>
</Card>
)}
{/* Organization Form */}
<form onSubmit={handleSubmit}>
<Stack spacing="lg">
{/* Basic Information */}
<Card>
<CardHeader>
<CardTitle>{t('organizationEdit.basicInfo')}</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField label={`${t('organizationEdit.organizationName')} *`} required>
<Input
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder={t('organizationEdit.namePlaceholder')}
required
/>
</FormField>
<FormField label={`${t('organizationEdit.sector')} *`} required>
<Select
value={formData.sector}
onValueChange={(value) => handleInputChange('sector', value)}
options={sectorOptions}
placeholder={t('organizationEdit.selectSector')}
required
/>
</FormField>
<FormField label={t('organizationEdit.subtype')}>
<Select
value={formData.subtype}
onValueChange={(value) => handleInputChange('subtype', value)}
options={subtypeOptions}
placeholder={t('organizationEdit.selectSubtype')}
/>
</FormField>
<FormField label={t('organizationEdit.description')}>
<Textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder={t('organizationEdit.descriptionPlaceholder')}
rows={4}
/>
</FormField>
</Stack>
</CardContent>
</Card>
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle>{t('organizationEdit.contactInfo')}</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField label={t('organizationEdit.website')}>
<Input
type="url"
value={formData.website}
onChange={(e) => handleInputChange('website', e.target.value)}
placeholder={t('organizationEdit.websitePlaceholder')}
/>
</FormField>
<FormField label={t('organizationEdit.address')}>
<Textarea
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder={t('organizationEdit.addressPlaceholder')}
rows={3}
/>
</FormField>
</Stack>
</CardContent>
</Card>
{/* Form Actions */}
<Card>
<CardContent className="pt-6">
<Flex align="center" justify="center" gap="md">
<Button type="button" variant="outline" onClick={handleCancel}>
{t('common.cancel')}
</Button>
<Button type="submit" disabled={isSubmitting}>
{isEditing
? t('organizationEdit.saveChanges')
: t('organizationEdit.createOrganization')}
</Button>
</Flex>
</CardContent>
</Card>
</Stack>
</form>
</Stack>
</Container>
</MainLayout>
);
};
export default OrganizationEditPage;