feat: add phone number filter and related translations in project list

This commit is contained in:
2025-10-17 16:00:44 +02:00
parent bd0345df1a
commit 27247477c9
2 changed files with 142 additions and 28 deletions

View File

@@ -20,15 +20,28 @@ export default function ProjectListPage() {
const [projects, setProjects] = useState([]);
const [searchTerm, setSearchTerm] = 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',
type: 'all',
customer: 'all',
mine: false
mine: false,
phoneOnly: savedPhoneOnly
};
});
const [customers, setCustomers] = useState([]);
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(() => {
fetch("/api/projects")
@@ -74,17 +87,37 @@ export default function ProjectListPage() {
// Apply search term
if (searchTerm.trim()) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter((project) => {
return (
const searchNormalized = normalizeString(searchLower);
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.wp?.toLowerCase().includes(searchLower) ||
project.plot?.toLowerCase().includes(searchLower) ||
project.investment_number?.toLowerCase().includes(searchLower) ||
project.address?.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);
@@ -107,10 +140,19 @@ export default function ProjectListPage() {
};
const handleFilterChange = (filterType, value) => {
setFilters(prev => ({
setFilters(prev => {
const newFilters = {
...prev,
[filterType]: value
}));
};
// Save phoneOnly filter to localStorage
if (filterType === 'phoneOnly') {
localStorage.setItem('projectsPhoneOnlyFilter', value.toString());
}
return newFilters;
});
};
const clearAllFilters = () => {
@@ -118,8 +160,10 @@ export default function ProjectListPage() {
status: 'all',
type: 'all',
customer: 'all',
mine: false
mine: false,
phoneOnly: false
});
localStorage.setItem('projectsPhoneOnlyFilter', 'false');
setSearchTerm('');
};
@@ -149,7 +193,7 @@ export default function ProjectListPage() {
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 = () => {
let count = 0;
@@ -157,6 +201,7 @@ export default function ProjectListPage() {
if (filters.type !== 'all') count++;
if (filters.customer !== 'all') count++;
if (filters.mine) count++;
if (filters.phoneOnly) count++;
if (searchTerm.trim()) count++;
return count;
};
@@ -366,6 +411,33 @@ export default function ProjectListPage() {
</select>
</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 && (
<button
onClick={() => handleFilterChange('mine', !filters.mine)}
@@ -396,6 +468,7 @@ export default function ProjectListPage() {
</div>
</div>
</div>
</div>
{/* Desktop always visible content */}
<div className="hidden md:block">
@@ -455,6 +528,32 @@ export default function ProjectListPage() {
</select>
</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 && (
<button
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`}
</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
variant="ghost"
size="sm"
@@ -617,12 +716,24 @@ export default function ProjectListPage() {
key={project.project_id}
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"
}`}
} ${project._matchedOnContact && filters.phoneOnly ? 'border-l-4 border-l-blue-500' : ''}`}
>
<td className="px-1 py-3">
<div className="flex items-center gap-1">
<Badge variant="secondary" size="sm" className="text-xs">
{project.project_number}
</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 className="px-2 py-3 w-[200px] md:w-[250px]">
<Link

View File

@@ -208,6 +208,9 @@ const translations = {
investmentNumber: "Numer inwestycji",
enterInvestmentNumber: "Wprowadź numer inwestycji",
enterWP: "Wprowadź WP",
contactNumberMatch: "Znaleziono numer kontaktowy",
phoneSearchEnabled: "Wyszukiwanie po numerze włączone",
phoneSearchDisabled: "Wyszukiwanie po numerze wyłączone",
placeholders: {
contact: "Wprowadź dane kontaktowe",
coordinates: "np. 49.622958,20.629562",