mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Replace all 'any' types with proper TypeScript interfaces - Fix React hooks setState in useEffect issues with lazy initialization - Remove unused variables and imports across all files - Fix React Compiler memoization dependency issues - Add comprehensive i18n translation keys for admin interfaces - Apply consistent prettier formatting throughout codebase - Clean up unused bulk editing functionality - Improve type safety and code quality across frontend Files changed: 39 - ImpactMetrics.tsx: Fixed any types and interfaces - AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars - LocalizationUIPage.tsx: Fixed memoization, added translations - LocalizationDataPage.tsx: Added type safety and translations - And 35+ other files with various lint fixes
380 lines
13 KiB
TypeScript
380 lines
13 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 { 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 { 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;
|