Skip to content

Commit 02065f5

Browse files
feat: melhoria na UI de lista de itens para evitar duplicação (#275)
## Related Issue Closes #254 ## Overall Este PR tem como objetivo alterar a forma como é feita a busca de itens na tela de abrigo para evitar a duplicação de itens. As alterações propostas incluem: - Remove o botão de cadastrar novo item no topo da página - Adiciona autocomplete para busca de itens - Adiciona opção de cadastrar novo item no autocomplete ## Screen recording https://github.com/SOS-RS/frontend/assets/8760873/74db3fa0-c45c-4b7a-afb2-d3d5279c0106
2 parents 286c867 + 97fb443 commit 02065f5

File tree

5 files changed

+155
-35
lines changed

5 files changed

+155
-35
lines changed

src/pages/EditShelterSupply/EditShelterSupply.tsx

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { ChevronLeft, PlusCircle } from 'lucide-react';
1+
import { ChevronLeft } from 'lucide-react';
22
import { useNavigate, useParams } from 'react-router-dom';
33
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
44

5-
import { DialogSelector, Header, LoadingScreen, TextField } from '@/components';
5+
import { DialogSelector, Header, LoadingScreen } from '@/components';
66
import { Button } from '@/components/ui/button';
77
import { useShelter, useSupplies, useThrottle } from '@/hooks';
88
import { group, normalizedCompare } from '@/lib/utils';
9-
import { SupplyRow } from './components';
9+
import { SupplyRow, SupplySearch } from './components';
1010
import { IDialogSelectorProps } from '@/components/DialogSelector/types';
1111
import { ISupplyRowItemProps } from './components/SupplyRow/types';
1212
import { ShelterSupplyServices } from '@/service';
@@ -25,32 +25,58 @@ const EditShelterSupply = () => {
2525
const [filteredSupplies, setFilteredSupplies] = useState<IUseSuppliesData[]>(
2626
[]
2727
);
28-
const [searchValue, setSearchValue] = useState<string>('');
28+
const [searchedSupplies, setSearchedSupplies] = useState<IUseSuppliesData[]>(
29+
[]
30+
);
31+
const shelterSupplyData = useMemo(() => {
32+
return (shelter?.shelterSupplies ?? []).reduce(
33+
(prev, current) => ({ ...prev, [current.supply.id]: current }),
34+
{} as Record<string, IUseShelterDataSupply>
35+
);
36+
}, [shelter?.shelterSupplies]);
37+
38+
const [, setSearchSupplies] = useThrottle<string>(
39+
{
40+
throttle: 200,
41+
callback: (value) => {
42+
if (value) {
43+
const filteredSupplies = supplies.filter((s) =>
44+
normalizedCompare(s.name, value)
45+
);
46+
setSearchedSupplies(filteredSupplies);
47+
} else {
48+
setSearchedSupplies([]);
49+
setSearch('');
50+
}
51+
},
52+
},
53+
[supplies]
54+
);
55+
2956
const [, setSearch] = useThrottle<string>(
3057
{
3158
throttle: 400,
32-
callback: (v) => {
33-
if (v) {
34-
setFilteredSupplies(
35-
supplies.filter((s) => normalizedCompare(s.name, v))
59+
callback: (value) => {
60+
if (value) {
61+
const filteredSupplies = supplies.filter((s) =>
62+
normalizedCompare(s.name, value)
3663
);
37-
} else setFilteredSupplies(supplies);
64+
setFilteredSupplies(filteredSupplies);
65+
} else {
66+
const storedSupplies = supplies.filter((s) => !!shelterSupplyData[s.id]);
67+
setFilteredSupplies(storedSupplies);
68+
}
3869
},
3970
},
40-
[supplies]
71+
[supplies, shelterSupplyData]
4172
);
4273
const [modalOpened, setModalOpened] = useState<boolean>(false);
4374
const [loadingSave, setLoadingSave] = useState<boolean>(false);
4475
const [modalData, setModalData] = useState<Pick<
4576
IDialogSelectorProps,
4677
'value' | 'onSave' | 'quantity'
4778
> | null>();
48-
const shelterSupplyData = useMemo(() => {
49-
return (shelter?.shelterSupplies ?? []).reduce(
50-
(prev, current) => ({ ...prev, [current.supply.id]: current }),
51-
{} as Record<string, IUseShelterDataSupply>
52-
);
53-
}, [shelter?.shelterSupplies]);
79+
5480
const supplyGroups = useMemo(
5581
() =>
5682
group<IUseSuppliesData>(filteredSupplies ?? [], 'supplyCategory.name'),
@@ -112,8 +138,9 @@ const EditShelterSupply = () => {
112138
);
113139

114140
useEffect(() => {
115-
setFilteredSupplies(supplies);
116-
}, [supplies]);
141+
const storedSupplies = supplies.filter((s) => !!shelterSupplyData[s.id]);
142+
setFilteredSupplies(storedSupplies);
143+
}, [supplies, shelterSupplyData]);
117144

118145
if (loading) return <LoadingScreen />;
119146

@@ -163,27 +190,26 @@ const EditShelterSupply = () => {
163190
<div className="p-4 flex flex-col max-w-5xl w-full gap-3 items-start">
164191
<h6 className="text-2xl font-semibold">Editar itens do abrigo</h6>
165192
<p className="text-muted-foreground">
166-
Para cada item da lista abaixo, informe a disponibilidade no abrigo
167-
selecionado
193+
Antes de adicionar um novo item, confira na busca abaixo se ele já não foi cadastrado.
168194
</p>
169-
<Button
170-
variant="ghost"
171-
className="flex gap-2 text-blue-500 [&_svg]:stroke-blue-500 font-medium text-lg hover:text-blue-600"
172-
onClick={() => navigate(`/abrigo/${shelterId}/item/cadastrar`)}
173-
>
174-
<PlusCircle />
175-
Cadastrar novo item
176-
</Button>
177195
<div className="w-full my-2">
178-
<TextField
179-
label="Buscar"
180-
value={searchValue}
181-
onChange={(ev) => {
182-
setSearchValue(ev.target.value);
183-
setSearch(ev.target.value);
196+
<SupplySearch
197+
supplyItems={searchedSupplies}
198+
limit={5}
199+
onSearch={(value) =>
200+
setSearchSupplies(value)
201+
}
202+
onSelectItem={(item) => {
203+
setSearch(item.name);
204+
setSearchedSupplies([]);
184205
}}
206+
onAddNewItem={() => navigate(`/abrigo/${shelterId}/item/cadastrar`)}
185207
/>
186208
</div>
209+
210+
<p className="text-muted-foreground mt-3">
211+
Para cada item da lista abaixo, informe a disponibilidade no abrigo selecionado.
212+
</p>
187213
<div className="flex flex-col gap-2 w-full my-4">
188214
{Object.entries(supplyGroups).map(([key, values], idx) => {
189215
const items: ISupplyRowItemProps[] = values
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Input } from '@/components/ui/input';
2+
import { IUseSuppliesData } from '@/hooks/useSupplies/types';
3+
import { Search, PlusCircle, X } from 'lucide-react';
4+
import { useState } from 'react';
5+
import { Fragment } from 'react/jsx-runtime';
6+
import {ISupplySearchProps} from './types';
7+
8+
export const SupplySearch = ({
9+
supplyItems,
10+
limit = 10,
11+
onSearch,
12+
onSelectItem,
13+
onAddNewItem
14+
}: ISupplySearchProps) => {
15+
const [searchValue, setSearchValue] = useState<string>('');
16+
const [selectedItem, setSelectedItem] = useState<IUseSuppliesData | null>(null);
17+
18+
function onChangeInputHandler(event: React.ChangeEvent<HTMLInputElement>) {
19+
setSearchValue(event.target.value);
20+
onSearch(event.target.value);
21+
}
22+
23+
function onSelectItemHandler(item: IUseSuppliesData) {
24+
setSearchValue(item.name);
25+
setSelectedItem(item);
26+
onSelectItem(item);
27+
}
28+
29+
function onAddNewItemHandler() {
30+
setSelectedItem(null);
31+
onAddNewItem();
32+
}
33+
34+
function onClearClickHandler() {
35+
setSelectedItem(null);
36+
setSearchValue('');
37+
onSearch('');
38+
}
39+
40+
return (
41+
<Fragment>
42+
<div
43+
className="flex items-center rounded-md border border-input px-3 h-10"
44+
cmdk-input-wrapper=""
45+
>
46+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
47+
<Input
48+
type="text"
49+
className="outline-none border-none focus-visible:ring-transparent h-8"
50+
placeholder="Buscar itens..."
51+
value={searchValue}
52+
onChange={onChangeInputHandler}
53+
/>
54+
<X className="h-4 w-4 ml-2 hover:cursor-pointer" onClick={onClearClickHandler} />
55+
</div>
56+
57+
{!!searchValue && !selectedItem ? (
58+
<div className="flex-col items-center rounded-md border border-input p-3 bg-slate-50 mt-1">
59+
{supplyItems.slice(0, limit).map((item) => (
60+
<div
61+
key={item.id}
62+
className="h-10 flex items-center rounded-md p-2 hover:bg-slate-100 hover:cursor-pointer"
63+
onClick={() => onSelectItemHandler(item)}
64+
>
65+
<span className="text-sm">{item.name}</span>
66+
</div>
67+
))}
68+
<div
69+
className="h-10 flex items-center rounded-md p-2 hover:bg-slate-100 hover:cursor-pointer"
70+
onClick={onAddNewItemHandler}
71+
>
72+
<div className="flex gap-2 items-center">
73+
<PlusCircle size={16} color="#0284c7" />
74+
<span className="text-sm text-sky-600">Cadastrar novo item</span>
75+
</div>
76+
</div>
77+
</div>
78+
) : null}
79+
</Fragment>
80+
);
81+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SupplySearch } from './SupplySearch';
2+
3+
export { SupplySearch };
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IUseSuppliesData } from "@/hooks/useSupplies/types";
2+
3+
export interface ISupplySearchProps {
4+
supplyItems: IUseSuppliesData[];
5+
limit?: number;
6+
onSearch: (value: string) => void;
7+
onSelectItem: (item: IUseSuppliesData) => void;
8+
onAddNewItem: () => void;
9+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SupplyRow } from './SupplyRow';
22
import { SupplyRowInfo } from './SupplyRowInfo';
3+
import { SupplySearch } from './SupplySearch';
34

4-
export { SupplyRowInfo, SupplyRow };
5+
export { SupplyRowInfo, SupplyRow, SupplySearch };

0 commit comments

Comments
 (0)