mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
395 lines
14 KiB
TypeScript
395 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 { 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">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.organizationName')} *
|
|
</label>
|
|
<Input
|
|
value={formData.name}
|
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
placeholder={t('organizationEdit.namePlaceholder')}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.sector')} *
|
|
</label>
|
|
<Select
|
|
value={formData.sector}
|
|
onValueChange={(value) => handleInputChange('sector', value)}
|
|
options={sectorOptions}
|
|
placeholder={t('organizationEdit.selectSector')}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.subtype')}
|
|
</label>
|
|
<Select
|
|
value={formData.subtype}
|
|
onValueChange={(value) => handleInputChange('subtype', value)}
|
|
options={subtypeOptions}
|
|
placeholder={t('organizationEdit.selectSubtype')}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.description')}
|
|
</label>
|
|
<Textarea
|
|
value={formData.description}
|
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
placeholder={t('organizationEdit.descriptionPlaceholder')}
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Contact Information */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('organizationEdit.contactInfo')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Stack spacing="md">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.website')}
|
|
</label>
|
|
<Input
|
|
type="url"
|
|
value={formData.website}
|
|
onChange={(e) => handleInputChange('website', e.target.value)}
|
|
placeholder={t('organizationEdit.websitePlaceholder')}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('organizationEdit.address')}
|
|
</label>
|
|
<Textarea
|
|
value={formData.address}
|
|
onChange={(e) => handleInputChange('address', e.target.value)}
|
|
placeholder={t('organizationEdit.addressPlaceholder')}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</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;
|