feat: add phone number filter and related translations in project list
This commit is contained in:
@@ -20,15 +20,28 @@ export default function ProjectListPage() {
|
|||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [filteredProjects, setFilteredProjects] = useState([]);
|
const [filteredProjects, setFilteredProjects] = useState([]);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState(() => {
|
||||||
|
// Load phoneOnly filter from localStorage
|
||||||
|
const savedPhoneOnly = typeof window !== 'undefined'
|
||||||
|
? localStorage.getItem('projectsPhoneOnlyFilter') === 'true'
|
||||||
|
: false;
|
||||||
|
return {
|
||||||
status: 'all',
|
status: 'all',
|
||||||
type: 'all',
|
type: 'all',
|
||||||
customer: 'all',
|
customer: 'all',
|
||||||
mine: false
|
mine: false,
|
||||||
|
phoneOnly: savedPhoneOnly
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
const [filtersExpanded, setFiltersExpanded] = useState(true); // Start expanded on mobile so users know filters exist
|
const [filtersExpanded, setFiltersExpanded] = useState(true); // Start expanded on mobile so users know filters exist
|
||||||
|
const [searchMatchType, setSearchMatchType] = useState(null); // Track what type of match was found
|
||||||
|
|
||||||
|
// Helper function to normalize strings by removing spaces
|
||||||
|
const normalizeString = (str) => {
|
||||||
|
return str?.replace(/\s+/g, '') || '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/projects")
|
fetch("/api/projects")
|
||||||
@@ -74,17 +87,37 @@ export default function ProjectListPage() {
|
|||||||
// Apply search term
|
// Apply search term
|
||||||
if (searchTerm.trim()) {
|
if (searchTerm.trim()) {
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const searchLower = searchTerm.toLowerCase();
|
||||||
filtered = filtered.filter((project) => {
|
const searchNormalized = normalizeString(searchLower);
|
||||||
return (
|
let hasContactMatch = false;
|
||||||
|
|
||||||
|
filtered = filtered.map((project) => {
|
||||||
|
const isContactMatch = normalizeString(project.contact?.toLowerCase()).includes(searchNormalized);
|
||||||
|
if (isContactMatch) hasContactMatch = true;
|
||||||
|
|
||||||
|
// Add a flag to mark projects that matched on contact
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
_matchedOnContact: isContactMatch
|
||||||
|
};
|
||||||
|
}).filter((project) => {
|
||||||
|
const baseMatches =
|
||||||
project.project_name?.toLowerCase().includes(searchLower) ||
|
project.project_name?.toLowerCase().includes(searchLower) ||
|
||||||
project.wp?.toLowerCase().includes(searchLower) ||
|
project.wp?.toLowerCase().includes(searchLower) ||
|
||||||
project.plot?.toLowerCase().includes(searchLower) ||
|
project.plot?.toLowerCase().includes(searchLower) ||
|
||||||
project.investment_number?.toLowerCase().includes(searchLower) ||
|
project.investment_number?.toLowerCase().includes(searchLower) ||
|
||||||
project.address?.toLowerCase().includes(searchLower) ||
|
project.address?.toLowerCase().includes(searchLower) ||
|
||||||
project.customer?.toLowerCase().includes(searchLower) ||
|
project.customer?.toLowerCase().includes(searchLower) ||
|
||||||
project.investor?.toLowerCase().includes(searchLower)
|
project.investor?.toLowerCase().includes(searchLower);
|
||||||
);
|
|
||||||
|
// Include contact matches only if phoneOnly is enabled
|
||||||
|
const contactMatch = filters.phoneOnly ? project._matchedOnContact : false;
|
||||||
|
|
||||||
|
return baseMatches || contactMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSearchMatchType(hasContactMatch ? 'contact' : null);
|
||||||
|
} else {
|
||||||
|
setSearchMatchType(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilteredProjects(filtered);
|
setFilteredProjects(filtered);
|
||||||
@@ -107,10 +140,19 @@ export default function ProjectListPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = (filterType, value) => {
|
const handleFilterChange = (filterType, value) => {
|
||||||
setFilters(prev => ({
|
setFilters(prev => {
|
||||||
|
const newFilters = {
|
||||||
...prev,
|
...prev,
|
||||||
[filterType]: value
|
[filterType]: value
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
// Save phoneOnly filter to localStorage
|
||||||
|
if (filterType === 'phoneOnly') {
|
||||||
|
localStorage.setItem('projectsPhoneOnlyFilter', value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFilters;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
const clearAllFilters = () => {
|
||||||
@@ -118,8 +160,10 @@ export default function ProjectListPage() {
|
|||||||
status: 'all',
|
status: 'all',
|
||||||
type: 'all',
|
type: 'all',
|
||||||
customer: 'all',
|
customer: 'all',
|
||||||
mine: false
|
mine: false,
|
||||||
|
phoneOnly: false
|
||||||
});
|
});
|
||||||
|
localStorage.setItem('projectsPhoneOnlyFilter', 'false');
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +193,7 @@ export default function ProjectListPage() {
|
|||||||
setFiltersExpanded(!filtersExpanded);
|
setFiltersExpanded(!filtersExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm.trim() !== '';
|
const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || filters.phoneOnly || searchTerm.trim() !== '';
|
||||||
|
|
||||||
const getActiveFilterCount = () => {
|
const getActiveFilterCount = () => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -157,6 +201,7 @@ export default function ProjectListPage() {
|
|||||||
if (filters.type !== 'all') count++;
|
if (filters.type !== 'all') count++;
|
||||||
if (filters.customer !== 'all') count++;
|
if (filters.customer !== 'all') count++;
|
||||||
if (filters.mine) count++;
|
if (filters.mine) count++;
|
||||||
|
if (filters.phoneOnly) count++;
|
||||||
if (searchTerm.trim()) count++;
|
if (searchTerm.trim()) count++;
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
@@ -366,6 +411,33 @@ export default function ProjectListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('phoneOnly', !filters.phoneOnly)}
|
||||||
|
className={`
|
||||||
|
inline-flex items-center justify-center w-9 h-9 rounded-full text-sm font-medium transition-all
|
||||||
|
${filters.phoneOnly
|
||||||
|
? 'bg-blue-100 text-blue-700 border-2 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-700 border-2 border-gray-200 hover:border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={filters.phoneOnly ? (t('projects.phoneSearchEnabled') || 'Wyszukiwanie po numerze włączone') : (t('projects.phoneSearchDisabled') || 'Wyszukiwanie po numerze wyłączone')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{session?.user && (
|
{session?.user && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFilterChange('mine', !filters.mine)}
|
onClick={() => handleFilterChange('mine', !filters.mine)}
|
||||||
@@ -396,6 +468,7 @@ export default function ProjectListPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Desktop always visible content */}
|
{/* Desktop always visible content */}
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
@@ -455,6 +528,32 @@ export default function ProjectListPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('phoneOnly', !filters.phoneOnly)}
|
||||||
|
className={`
|
||||||
|
inline-flex items-center justify-center w-9 h-9 rounded-full text-sm font-medium transition-all
|
||||||
|
${filters.phoneOnly
|
||||||
|
? 'bg-blue-100 text-blue-700 border-2 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-700 border-2 border-gray-200 hover:border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={filters.phoneOnly ? (t('projects.phoneSearchEnabled') || 'Wyszukiwanie po numerze włączone') : (t('projects.phoneSearchDisabled') || 'Wyszukiwanie po numerze wyłączone')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{session?.user && (
|
{session?.user && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFilterChange('mine', !filters.mine)}
|
onClick={() => handleFilterChange('mine', !filters.mine)}
|
||||||
@@ -490,7 +589,7 @@ export default function ProjectListPage() {
|
|||||||
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
|
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && (
|
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || filters.phoneOnly || searchTerm) && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -617,12 +716,24 @@ export default function ProjectListPage() {
|
|||||||
key={project.project_id}
|
key={project.project_id}
|
||||||
className={`border-b dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
|
className={`border-b dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
|
||||||
index % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-25 dark:bg-gray-750"
|
index % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-25 dark:bg-gray-750"
|
||||||
}`}
|
} ${project._matchedOnContact && filters.phoneOnly ? 'border-l-4 border-l-blue-500' : ''}`}
|
||||||
>
|
>
|
||||||
<td className="px-1 py-3">
|
<td className="px-1 py-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<Badge variant="secondary" size="sm" className="text-xs">
|
<Badge variant="secondary" size="sm" className="text-xs">
|
||||||
{project.project_number}
|
{project.project_number}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{project._matchedOnContact && filters.phoneOnly && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center w-5 h-5 bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 rounded-full"
|
||||||
|
title={t('projects.contactNumberMatch') || 'Znaleziono numer kontaktowy'}
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2 py-3 w-[200px] md:w-[250px]">
|
<td className="px-2 py-3 w-[200px] md:w-[250px]">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ const translations = {
|
|||||||
investmentNumber: "Numer inwestycji",
|
investmentNumber: "Numer inwestycji",
|
||||||
enterInvestmentNumber: "Wprowadź numer inwestycji",
|
enterInvestmentNumber: "Wprowadź numer inwestycji",
|
||||||
enterWP: "Wprowadź WP",
|
enterWP: "Wprowadź WP",
|
||||||
|
contactNumberMatch: "Znaleziono numer kontaktowy",
|
||||||
|
phoneSearchEnabled: "Wyszukiwanie po numerze włączone",
|
||||||
|
phoneSearchDisabled: "Wyszukiwanie po numerze wyłączone",
|
||||||
placeholders: {
|
placeholders: {
|
||||||
contact: "Wprowadź dane kontaktowe",
|
contact: "Wprowadź dane kontaktowe",
|
||||||
coordinates: "np. 49.622958,20.629562",
|
coordinates: "np. 49.622958,20.629562",
|
||||||
|
|||||||
Reference in New Issue
Block a user