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 [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredProjects, setFilteredProjects] = useState([]);
|
||||
const [filters, setFilters] = useState({
|
||||
status: 'all',
|
||||
type: 'all',
|
||||
customer: 'all',
|
||||
mine: false
|
||||
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,
|
||||
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 => ({
|
||||
...prev,
|
||||
[filterType]: value
|
||||
}));
|
||||
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,16 +411,17 @@ export default function ProjectListPage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{session?.user && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleFilterChange('mine', !filters.mine)}
|
||||
onClick={() => handleFilterChange('phoneOnly', !filters.phoneOnly)}
|
||||
className={`
|
||||
inline-flex items-center space-x-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all
|
||||
${filters.mine
|
||||
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"
|
||||
@@ -387,12 +433,39 @@ export default function ProjectListPage() {
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
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>{t('projects.mine') || 'Tylko moje'}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{session?.user && (
|
||||
<button
|
||||
onClick={() => handleFilterChange('mine', !filters.mine)}
|
||||
className={`
|
||||
inline-flex items-center space-x-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all
|
||||
${filters.mine
|
||||
? '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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{t('projects.mine') || 'Tylko moje'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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"
|
||||
@@ -615,16 +714,28 @@ export default function ProjectListPage() {
|
||||
{filteredProjects.map((project, index) => (
|
||||
<tr
|
||||
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"
|
||||
}`}
|
||||
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>
|
||||
</td>
|
||||
<td className="px-2 py-3 w-[200px] md:w-[250px]">
|
||||
{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
|
||||
href={`/projects/${project.project_id}`}
|
||||
className="font-medium text-blue-600 hover:text-blue-800 transition-colors text-sm truncate block"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user