Skip to content

Commit

Permalink
Refactor locations section with dynamic sheet-based editing and impro…
Browse files Browse the repository at this point in the history
…ved validation

- Introduced OsblLocationSheet component for detailed location management
- Replaced complex card-based location form with a more user-friendly sheet interface
- Added dynamic location display with edit and remove functionality
- Improved location type and address validation
- Extracted location type list to constants file
- Updated location model and database constraints for more precise validation
- Enhanced MyAsyncSelect component to support pre-filled values
  • Loading branch information
mpressen committed Feb 3, 2025
1 parent dac71fe commit 66a06f3
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 223 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default function OsblDocumentSheet ({
<SelectTrigger className='w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent id={`document-type-${index}`}>
<SelectContent>
{DocumentTypeList.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
Expand All @@ -140,7 +140,7 @@ export default function OsblDocumentSheet ({
<MyInput
labelText='Nom du document *'
id={`document-name-${index}`}
type='string'
type='text'
value={sheetDocument.name ?? ''}
onChange={(e) => updateSheetDocument('name', e.target.value)}
required
Expand Down Expand Up @@ -176,7 +176,7 @@ export default function OsblDocumentSheet ({
<MyInput
labelText='Nom du document'
id={`document-name-${index}`}
type='string'
type='text'
value={sheetDocument.name ?? ''}
onChange={(e) => updateSheetDocument('name', e.target.value)}
/>
Expand Down
61 changes: 29 additions & 32 deletions app/frontend/components/pages/contribution/new/OsblDocuments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,39 +62,36 @@ export default function OsblDocuments ({ data, setData, errors, clearErrors, set

{documents.length > 0 && (
<div className='flex flex-col gap-4'>
{documents
.map((doc, index) => {
return (
<Sheet key={`document-${index}`}>
<div className='flex items-center justify-between p-4 border rounded-lg bg-white'>
<p>{getDocumentDisplayName(doc.document_attributes)}</p>
<div className='flex gap-2 '>
<SheetTrigger asChild>
<Button variant='outline' className='bg-white text-primary border-none'>
<PencilIcon />
</Button>
</SheetTrigger>
<Button
onClick={(e) => handleDocumentRemove(e, index)}
variant='outline'
className='bg-white text-red-500 border-none'
>
<TrashIcon className='w-4 h-4' />
</Button>
</div>
</div>
{documents.map((doc, index) => (
<Sheet key={`document-${index}`}>
<div className='flex items-center justify-between p-4 border rounded-lg bg-white'>
<p>{getDocumentDisplayName(doc.document_attributes)}</p>
<div className='flex gap-2 '>
<SheetTrigger asChild>
<Button variant='outline' className='bg-white text-primary border-none'>
<PencilIcon />
</Button>
</SheetTrigger>
<Button
onClick={(e) => handleDocumentRemove(e, index)}
variant='outline'
className='bg-white text-red-500 border-none'
>
<TrashIcon className='w-4 h-4' />
</Button>
</div>
</div>

<OsblDocumentSheet
document={doc.document_attributes}
index={index}
onUpdate={(doc) => addDocument(doc, index)}
errors={errors}
clearErrors={clearErrors}
setError={setError}
/>
</Sheet>
)
})}
<OsblDocumentSheet
document={doc.document_attributes}
index={index}
onUpdate={(doc) => addDocument(doc, index)}
errors={errors}
clearErrors={clearErrors}
setError={setError}
/>
</Sheet>
))}
</div>
)}
</div>
Expand Down
220 changes: 220 additions & 0 deletions app/frontend/components/pages/contribution/new/OsblLocationSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { ReactElement, useRef, useState } from 'react'
import { Location } from '@/pages/Contribution/types'
import { Button } from '@/components/ui/button'
import MyInput from '@/components/forms/MyInput'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { CheckIcon, ChevronDown, X } from 'lucide-react'
import {
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetDescription
} from '@/components/ui/sheet'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/components/ui/collapsible'
import MyAsyncSelect from '@/components/shared/MyAsyncSelect'
import deepCleanData from '@/lib/deepCleanData'
import { LocationTypeList } from '@/lib/constants'
import { Label } from '@/components/ui/label'

interface Props {
location: Partial<Location>
index: number
onUpdate: (location: Location) => void
hasSiegeSocial: boolean
}

export default function OsblLocationSheet ({
location,
index,
onUpdate,
hasSiegeSocial
}: Props): ReactElement {
const formRef = useRef<HTMLFormElement>(null)
const [sheetLocation, setSheetLocation] = useState<Partial<Location>>(location)
const [isOpen, setIsOpen] = useState(() => {
const checks = [
sheetLocation.name !== undefined,
sheetLocation.address_attributes?.additional_info !== undefined,
sheetLocation.website !== undefined,
sheetLocation.description !== undefined
]
return checks.some(check => check)
})

function updateSheetLocation (field: keyof Location, value: any): void {
setSheetLocation(prev => ({
...prev,
[field]: value
}))
}

function handleAddressChange (value: any, field?: string): void {
setSheetLocation(prev => ({
...prev,
address_attributes: field != null
? { ...prev.address_attributes, [field]: value }
: value
}))
}

function handleSubmit (e: React.MouseEvent<HTMLButtonElement>): void {
if (formRef.current?.reportValidity() === false) {
e.preventDefault()
return
}

onUpdate(deepCleanData(sheetLocation))
}

function getLabel (): string {
return [
sheetLocation.address_attributes?.street_number,
sheetLocation.address_attributes?.street_name,
sheetLocation.address_attributes?.postal_code,
sheetLocation.address_attributes?.city
].filter(Boolean).join(' ')
}

return (
<SheetContent className='w-full sm:max-w-[600px] overflow-y-auto'>
<form ref={formRef}>
<SheetHeader>
<SheetTitle>Lieu</SheetTitle>
<SheetDescription className='sr-only'>
Renseignez les informations du lieu.
</SheetDescription>
</SheetHeader>

<div className='flex flex-col gap-8 mt-8'>
<div className='flex flex-col gap-4'>
<Label>Type de lieu *</Label>
<Select
value={sheetLocation.type}
onValueChange={(value) => updateSheetLocation('type', value)}
required
>
<SelectTrigger className='w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
{LocationTypeList
.filter(type =>
type.value !== 'siege_social' || !hasSiegeSocial || location.type === 'siege_social'
)
.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{['antenne_locale', 'lieu_d_activite', 'autre'].includes(sheetLocation.type ?? '') && (
<MyInput
labelText='Nom du lieu *'
id={`location-name-${index}`}
type='text'
value={sheetLocation.name ?? ''}
onChange={(e) => updateSheetLocation('name', e.target.value)}
required
/>
)}

<div className='flex flex-col gap-4'>
<Label>Adresse *</Label>
<MyAsyncSelect
setData={(_, value) => handleAddressChange(value)}
attributeName='address_attributes'
required
value={getLabel()}
/>
</div>

<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<div className='flex justify-between items-center cursor-pointer text-muted-foreground hover:text-muted-foreground/80'>
<span>Informations additionnelles</span>
<ChevronDown className={`transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'>
<div className='flex flex-col gap-8 mt-8'>
{sheetLocation.type === 'siege_social' && (
<MyInput
labelText='Nom du lieu'
id={`location-name-${index}`}
type='text'
value={sheetLocation.name ?? ''}
onChange={(e) => updateSheetLocation('name', e.target.value)}
/>
)}

<MyInput
labelText="Complément d'adresse"
type='text'
id={`location-additional-address-info-${index}`}
value={sheetLocation.address_attributes?.additional_info ?? ''}
onChange={(e) => handleAddressChange(e.target.value, 'additional_info')}
/>

<MyInput
labelText='Site web'
type='url'
id={`location-website-${index}`}
value={sheetLocation.website ?? ''}
onChange={(e) => updateSheetLocation('website', e.target.value)}
/>

<div className='flex flex-col gap-4'>
<label htmlFor={`location-description-${index}`} className='text-sm font-medium'>
Description du lieu
</label>
<Textarea
id={`location-description-${index}`}
value={sheetLocation.description ?? ''}
maxLength={300}
onChange={(e) => updateSheetLocation('description', e.target.value)}
className='bg-white focus-visible:ring-0 focus-visible:border-primary placeholder:text-ellipsis placeholder:text-xs md:placeholder:text-sm focus-visible:ring-offset-0 w-full h-40'
/>
<div className='text-xs text-right'>
{sheetLocation.description?.length ?? 0}/300 caractères
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>

<SheetFooter className='mt-16 gap-y-2'>
<SheetClose asChild>
<Button variant='ghost' size='lg'>
<X className='mr-2' />
Annuler
</Button>
</SheetClose>

<SheetClose asChild>
<Button onClick={handleSubmit} variant='default' size='lg'>
<CheckIcon className='mr-2' />
Valider
</Button>
</SheetClose>
</SheetFooter>
</form>
</SheetContent>
)
}
Loading

0 comments on commit 66a06f3

Please sign in to comment.