KI-AGENT: Share-Upload an FEDEO-Design und Listenfilter angleichen
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
|
|
||||||
import { createProject, Customer, fetchCustomers, fetchPlants, fetchProjects, Plant, Project } from '@/src/lib/api';
|
import { createProject, Customer, fetchCustomers, fetchPlants, fetchProjects, Plant, Project } from '@/src/lib/api';
|
||||||
|
import { filterProjects, getActiveProjectPhase, isProjectCompleted } from '@/src/lib/resource-list-filters';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
const PRIMARY = '#69c350';
|
const PRIMARY = '#69c350';
|
||||||
@@ -24,20 +25,6 @@ function getProjectLine(project: Project): string {
|
|||||||
return project.name;
|
return project.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActivePhaseLabel(project: Project): string {
|
|
||||||
const explicit = String(project.active_phase || '').trim();
|
|
||||||
if (explicit) return explicit;
|
|
||||||
|
|
||||||
const phases = Array.isArray(project.phases) ? project.phases : [];
|
|
||||||
const active = phases.find((phase: any) => phase?.active);
|
|
||||||
return String(active?.label || '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProjectCompletedByPhase(project: Project): boolean {
|
|
||||||
const phase = getActivePhaseLabel(project).toLowerCase();
|
|
||||||
return phase === 'abgeschlossen';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectsScreen() {
|
export default function ProjectsScreen() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
@@ -64,31 +51,10 @@ export default function ProjectsScreen() {
|
|||||||
const [customerSearch, setCustomerSearch] = useState('');
|
const [customerSearch, setCustomerSearch] = useState('');
|
||||||
const [plantSearch, setPlantSearch] = useState('');
|
const [plantSearch, setPlantSearch] = useState('');
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(
|
||||||
const terms = search
|
() => filterProjects(projects, search, showArchived),
|
||||||
.trim()
|
[projects, search, showArchived]
|
||||||
.toLowerCase()
|
);
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return projects.filter((project) => {
|
|
||||||
if (!showArchived && isProjectCompletedByPhase(project)) return false;
|
|
||||||
if (terms.length === 0) return true;
|
|
||||||
|
|
||||||
const haystack = [
|
|
||||||
project.name,
|
|
||||||
project.projectNumber,
|
|
||||||
project.notes,
|
|
||||||
project.customerRef,
|
|
||||||
project.active_phase,
|
|
||||||
getActivePhaseLabel(project),
|
|
||||||
]
|
|
||||||
.map((value) => String(value || '').toLowerCase())
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return terms.every((term) => haystack.includes(term));
|
|
||||||
});
|
|
||||||
}, [projects, search, showArchived]);
|
|
||||||
|
|
||||||
const selectedCustomerLabel = useMemo(() => {
|
const selectedCustomerLabel = useMemo(() => {
|
||||||
if (!selectedCustomerId) return 'Kunde auswählen (optional)';
|
if (!selectedCustomerId) return 'Kunde auswählen (optional)';
|
||||||
@@ -241,10 +207,10 @@ export default function ProjectsScreen() {
|
|||||||
<Pressable key={String(project.id)} style={styles.row} onPress={() => router.push(`/project/${project.id}`)}>
|
<Pressable key={String(project.id)} style={styles.row} onPress={() => router.push(`/project/${project.id}`)}>
|
||||||
<View style={styles.rowHeader}>
|
<View style={styles.rowHeader}>
|
||||||
<Text style={styles.rowTitle} numberOfLines={1}>{getProjectLine(project)}</Text>
|
<Text style={styles.rowTitle} numberOfLines={1}>{getProjectLine(project)}</Text>
|
||||||
{isProjectCompletedByPhase(project) ? <Text style={styles.archivedBadge}>Abgeschlossen</Text> : null}
|
{isProjectCompleted(project) ? <Text style={styles.archivedBadge}>Abgeschlossen</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
{getActivePhaseLabel(project) ? (
|
{getActiveProjectPhase(project) ? (
|
||||||
<Text style={styles.phaseText} numberOfLines={1}>Phase: {getActivePhaseLabel(project)}</Text>
|
<Text style={styles.phaseText} numberOfLines={1}>Phase: {getActiveProjectPhase(project)}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{project.notes ? <Text style={styles.rowSubtitle} numberOfLines={1}>{String(project.notes)}</Text> : null}
|
{project.notes ? <Text style={styles.rowSubtitle} numberOfLines={1}>{String(project.notes)}</Text> : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
|
||||||
import { createCustomer, Customer, fetchCustomers } from '@/src/lib/api';
|
import { createCustomer, Customer, fetchCustomers } from '@/src/lib/api';
|
||||||
|
import { filterCustomers } from '@/src/lib/resource-list-filters';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
const PRIMARY = '#69c350';
|
const PRIMARY = '#69c350';
|
||||||
@@ -37,20 +38,10 @@ export default function CustomersScreen() {
|
|||||||
const [numberInput, setNumberInput] = useState('');
|
const [numberInput, setNumberInput] = useState('');
|
||||||
const [notesInput, setNotesInput] = useState('');
|
const [notesInput, setNotesInput] = useState('');
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(
|
||||||
const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
() => filterCustomers(customers, search, showArchived),
|
||||||
|
[customers, search, showArchived]
|
||||||
return customers.filter((customer) => {
|
);
|
||||||
if (!showArchived && customer.archived) return false;
|
|
||||||
if (terms.length === 0) return true;
|
|
||||||
|
|
||||||
const haystack = [customer.name, customer.customerNumber, customer.notes]
|
|
||||||
.map((value) => String(value || '').toLowerCase())
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return terms.every((term) => haystack.includes(term));
|
|
||||||
});
|
|
||||||
}, [customers, search, showArchived]);
|
|
||||||
|
|
||||||
const load = useCallback(async (showSpinner = true) => {
|
const load = useCallback(async (showSpinner = true) => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -15,16 +15,11 @@ import {
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
|
||||||
import { createPlant, Customer, fetchCustomers, fetchPlants, Plant } from '@/src/lib/api';
|
import { createPlant, Customer, fetchCustomers, fetchPlants, Plant } from '@/src/lib/api';
|
||||||
|
import { filterPlants, getPlantCustomerName } from '@/src/lib/resource-list-filters';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
const PRIMARY = '#69c350';
|
const PRIMARY = '#69c350';
|
||||||
|
|
||||||
function getCustomerName(raw: Plant['customer']): string | null {
|
|
||||||
if (!raw) return null;
|
|
||||||
if (typeof raw === 'object') return raw.name ? String(raw.name) : null;
|
|
||||||
return String(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlantsScreen() {
|
export default function PlantsScreen() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -47,20 +42,10 @@ export default function PlantsScreen() {
|
|||||||
const [pickerMode, setPickerMode] = useState<'customer' | null>(null);
|
const [pickerMode, setPickerMode] = useState<'customer' | null>(null);
|
||||||
const [customerSearch, setCustomerSearch] = useState('');
|
const [customerSearch, setCustomerSearch] = useState('');
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(
|
||||||
const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
() => filterPlants(plants, search, showArchived),
|
||||||
|
[plants, search, showArchived]
|
||||||
return plants.filter((plant) => {
|
);
|
||||||
if (!showArchived && plant.archived) return false;
|
|
||||||
if (terms.length === 0) return true;
|
|
||||||
|
|
||||||
const haystack = [plant.name, plant.description, getCustomerName(plant.customer)]
|
|
||||||
.map((value) => String(value || '').toLowerCase())
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return terms.every((term) => haystack.includes(term));
|
|
||||||
});
|
|
||||||
}, [plants, search, showArchived]);
|
|
||||||
|
|
||||||
const filteredCustomerOptions = useMemo(() => {
|
const filteredCustomerOptions = useMemo(() => {
|
||||||
const terms = customerSearch.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
const terms = customerSearch.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
@@ -189,8 +174,8 @@ export default function PlantsScreen() {
|
|||||||
<Text style={styles.rowTitle} numberOfLines={1}>{plant.name}</Text>
|
<Text style={styles.rowTitle} numberOfLines={1}>{plant.name}</Text>
|
||||||
{plant.archived ? <Text style={styles.badge}>Abgeschlossen</Text> : null}
|
{plant.archived ? <Text style={styles.badge}>Abgeschlossen</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
{getCustomerName(plant.customer) ? (
|
{getPlantCustomerName(plant.customer) ? (
|
||||||
<Text style={styles.rowSubtitle} numberOfLines={1}>Kunde: {getCustomerName(plant.customer)}</Text>
|
<Text style={styles.rowSubtitle} numberOfLines={1}>Kunde: {getPlantCustomerName(plant.customer)}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{plant.description ? <Text style={styles.rowSubtitle} numberOfLines={1}>{String(plant.description)}</Text> : null}
|
{plant.description ? <Text style={styles.rowSubtitle} numberOfLines={1}>{String(plant.description)}</Text> : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ import {
|
|||||||
uploadProjectFile,
|
uploadProjectFile,
|
||||||
} from '@/src/lib/api';
|
} from '@/src/lib/api';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
import {
|
||||||
|
filterCustomers,
|
||||||
|
filterPlants,
|
||||||
|
filterProjects,
|
||||||
|
getActiveProjectPhase,
|
||||||
|
getPlantCustomerName,
|
||||||
|
} from '@/src/lib/resource-list-filters';
|
||||||
|
|
||||||
type TargetType = 'project' | 'customer' | 'plant';
|
type TargetType = 'project' | 'customer' | 'plant';
|
||||||
type UploadTarget = Project | Customer | Plant;
|
type UploadTarget = Project | Customer | Plant;
|
||||||
@@ -36,6 +43,8 @@ const TARGET_TYPES: { key: TargetType; label: string; plural: string; icon: keyo
|
|||||||
{ key: 'plant', label: 'Objekt', plural: 'Objekte', icon: 'business-outline' },
|
{ key: 'plant', label: 'Objekt', plural: 'Objekte', icon: 'business-outline' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PRIMARY = '#69c350';
|
||||||
|
|
||||||
export default function ShareUploadScreen() {
|
export default function ShareUploadScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
@@ -60,10 +69,10 @@ export default function ShareUploadScreen() {
|
|||||||
setSearch('');
|
setSearch('');
|
||||||
|
|
||||||
const request = targetType === 'project'
|
const request = targetType === 'project'
|
||||||
? fetchProjects(token)
|
? fetchProjects(token, true)
|
||||||
: targetType === 'customer'
|
: targetType === 'customer'
|
||||||
? fetchCustomers(token)
|
? fetchCustomers(token, true)
|
||||||
: fetchPlants(token);
|
: fetchPlants(token, true);
|
||||||
|
|
||||||
void request
|
void request
|
||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
@@ -82,17 +91,11 @@ export default function ShareUploadScreen() {
|
|||||||
}, [targetType, token]);
|
}, [targetType, token]);
|
||||||
|
|
||||||
const filteredTargets = useMemo(() => {
|
const filteredTargets = useMemo(() => {
|
||||||
const term = search.trim().toLocaleLowerCase('de');
|
if (targetType === 'project') return filterProjects(targets as Project[], search);
|
||||||
if (!term) return targets;
|
if (targetType === 'customer') return filterCustomers(targets as Customer[], search);
|
||||||
return targets.filter((target) => {
|
if (targetType === 'plant') return filterPlants(targets as Plant[], search);
|
||||||
const number = 'projectNumber' in target
|
return [];
|
||||||
? target.projectNumber
|
}, [search, targets, targetType]);
|
||||||
: 'customerNumber' in target
|
|
||||||
? target.customerNumber
|
|
||||||
: null;
|
|
||||||
return `${target.name} ${number || ''}`.toLocaleLowerCase('de').includes(term);
|
|
||||||
});
|
|
||||||
}, [search, targets]);
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
resetShareIntent();
|
resetShareIntent();
|
||||||
@@ -144,7 +147,9 @@ export default function ShareUploadScreen() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<Ionicons name="document-outline" size={44} color="#9ca3af" />
|
<View style={styles.emptyIcon}>
|
||||||
|
<Ionicons name="document-outline" size={36} color={PRIMARY} />
|
||||||
|
</View>
|
||||||
<Text style={styles.emptyTitle}>Keine Dateien gefunden</Text>
|
<Text style={styles.emptyTitle}>Keine Dateien gefunden</Text>
|
||||||
<Text style={styles.emptyText}>Teile eine Datei oder ein Foto erneut mit FEDEO.</Text>
|
<Text style={styles.emptyText}>Teile eine Datei oder ein Foto erneut mit FEDEO.</Text>
|
||||||
<Pressable style={styles.primaryButton} onPress={close}>
|
<Pressable style={styles.primaryButton} onPress={close}>
|
||||||
@@ -159,7 +164,9 @@ export default function ShareUploadScreen() {
|
|||||||
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
||||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||||
<View style={styles.fileSummary}>
|
<View style={styles.fileSummary}>
|
||||||
<Ionicons name="documents-outline" size={24} color="#2563eb" />
|
<View style={styles.summaryIcon}>
|
||||||
|
<Ionicons name="documents-outline" size={22} color="#3d7a30" />
|
||||||
|
</View>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<Text style={styles.summaryTitle}>{files.length} {files.length === 1 ? 'Datei' : 'Dateien'} ausgewählt</Text>
|
<Text style={styles.summaryTitle}>{files.length} {files.length === 1 ? 'Datei' : 'Dateien'} ausgewählt</Text>
|
||||||
<Text style={styles.summaryText} numberOfLines={2}>{files.map((file) => file.fileName).join(', ')}</Text>
|
<Text style={styles.summaryText} numberOfLines={2}>{files.map((file) => file.fileName).join(', ')}</Text>
|
||||||
@@ -173,7 +180,7 @@ export default function ShareUploadScreen() {
|
|||||||
<View style={styles.typeGrid}>
|
<View style={styles.typeGrid}>
|
||||||
{TARGET_TYPES.map((type) => (
|
{TARGET_TYPES.map((type) => (
|
||||||
<Pressable key={type.key} style={styles.typeCard} onPress={() => setTargetType(type.key)}>
|
<Pressable key={type.key} style={styles.typeCard} onPress={() => setTargetType(type.key)}>
|
||||||
<View style={styles.iconCircle}><Ionicons name={type.icon} size={26} color="#2563eb" /></View>
|
<View style={styles.iconCircle}><Ionicons name={type.icon} size={24} color="#3d7a30" /></View>
|
||||||
<Text style={styles.typeLabel}>{type.label}</Text>
|
<Text style={styles.typeLabel}>{type.label}</Text>
|
||||||
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -183,21 +190,24 @@ export default function ShareUploadScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pressable style={styles.backLink} onPress={() => setTargetType(null)} disabled={isUploading}>
|
<Pressable style={styles.backLink} onPress={() => setTargetType(null)} disabled={isUploading}>
|
||||||
<Ionicons name="chevron-back" size={18} color="#2563eb" />
|
<Ionicons name="chevron-back" size={18} color="#3d7a30" />
|
||||||
<Text style={styles.backLinkText}>Andere Zielart wählen</Text>
|
<Text style={styles.backLinkText}>Andere Zielart wählen</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={styles.heading}>{selectedType?.label} auswählen</Text>
|
<Text style={styles.heading}>{selectedType?.label} auswählen</Text>
|
||||||
<TextInput
|
<View style={styles.searchBox}>
|
||||||
value={search}
|
<Ionicons name="search" size={19} color="#6b7280" />
|
||||||
onChangeText={setSearch}
|
<TextInput
|
||||||
placeholder={`${selectedType?.plural || 'Ziele'} durchsuchen`}
|
value={search}
|
||||||
placeholderTextColor="#9ca3af"
|
onChangeText={setSearch}
|
||||||
style={styles.searchInput}
|
placeholder={`${selectedType?.plural || 'Ziele'} durchsuchen`}
|
||||||
editable={!isUploading}
|
placeholderTextColor="#9ca3af"
|
||||||
/>
|
style={styles.searchInput}
|
||||||
|
editable={!isUploading}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator style={styles.loader} color="#2563eb" />
|
<ActivityIndicator style={styles.loader} color={PRIMARY} />
|
||||||
) : loadError ? (
|
) : loadError ? (
|
||||||
<Text style={styles.errorText}>{loadError}</Text>
|
<Text style={styles.errorText}>{loadError}</Text>
|
||||||
) : filteredTargets.length === 0 ? (
|
) : filteredTargets.length === 0 ? (
|
||||||
@@ -210,13 +220,29 @@ export default function ShareUploadScreen() {
|
|||||||
: 'customerNumber' in target
|
: 'customerNumber' in target
|
||||||
? target.customerNumber
|
? target.customerNumber
|
||||||
: null;
|
: null;
|
||||||
|
const detail = targetType === 'project'
|
||||||
|
? getActiveProjectPhase(target as Project)
|
||||||
|
: targetType === 'plant'
|
||||||
|
? getPlantCustomerName((target as Plant).customer)
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<Pressable key={target.id} style={styles.targetRow} onPress={() => void uploadTo(target)} disabled={isUploading}>
|
<Pressable
|
||||||
|
key={target.id}
|
||||||
|
style={({ pressed }) => [styles.targetRow, pressed ? styles.targetRowPressed : null]}
|
||||||
|
onPress={() => void uploadTo(target)}
|
||||||
|
disabled={isUploading}>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<Text style={styles.targetName}>{target.name}</Text>
|
<Text style={styles.targetName}>{target.name}</Text>
|
||||||
{number ? <Text style={styles.targetNumber}>{String(number)}</Text> : null}
|
{number ? <Text style={styles.targetNumber}>Nr.: {String(number)}</Text> : null}
|
||||||
|
{detail ? (
|
||||||
|
<Text style={styles.targetNumber} numberOfLines={1}>
|
||||||
|
{targetType === 'project' ? `Phase: ${detail}` : `Kunde: ${detail}`}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<View style={styles.uploadIcon}>
|
||||||
|
<Ionicons name="cloud-upload-outline" size={21} color="#3d7a30" />
|
||||||
</View>
|
</View>
|
||||||
<Ionicons name="cloud-upload-outline" size={22} color="#2563eb" />
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -227,7 +253,7 @@ export default function ShareUploadScreen() {
|
|||||||
|
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<View style={styles.uploadOverlay}>
|
<View style={styles.uploadOverlay}>
|
||||||
<ActivityIndicator color="#2563eb" />
|
<ActivityIndicator color={PRIMARY} />
|
||||||
<Text style={styles.uploadText}>Upload läuft: {uploadedCount} von {files.length}</Text>
|
<Text style={styles.uploadText}>Upload läuft: {uploadedCount} von {files.length}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -241,35 +267,40 @@ export default function ShareUploadScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safeArea: { flex: 1, backgroundColor: '#f9fafb' },
|
safeArea: { flex: 1, backgroundColor: '#ffffff' },
|
||||||
content: { padding: 20, paddingBottom: 36, gap: 16 },
|
content: { paddingBottom: 28, gap: 14 },
|
||||||
flex: { flex: 1 },
|
flex: { flex: 1 },
|
||||||
fileSummary: { flexDirection: 'row', gap: 12, alignItems: 'center', padding: 16, borderRadius: 14, backgroundColor: '#eff6ff' },
|
fileSummary: { flexDirection: 'row', gap: 12, alignItems: 'center', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#d8edcf', backgroundColor: '#eff9ea' },
|
||||||
summaryTitle: { fontSize: 16, fontWeight: '700', color: '#1e3a8a' },
|
summaryIcon: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' },
|
||||||
summaryText: { marginTop: 3, fontSize: 13, color: '#3b4f78' },
|
summaryTitle: { fontSize: 15, fontWeight: '700', color: '#2f5f24' },
|
||||||
heading: { marginTop: 8, fontSize: 23, fontWeight: '800', color: '#111827' },
|
summaryText: { marginTop: 2, fontSize: 12, color: '#4f6f47' },
|
||||||
description: { marginTop: -10, fontSize: 15, color: '#6b7280' },
|
heading: { marginTop: 4, paddingHorizontal: 16, fontSize: 20, fontWeight: '700', color: '#111827' },
|
||||||
typeGrid: { gap: 12 },
|
description: { marginTop: -8, paddingHorizontal: 16, fontSize: 14, color: '#6b7280' },
|
||||||
typeCard: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16, borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 14, backgroundColor: '#fff' },
|
typeGrid: { borderTopWidth: 1, borderTopColor: '#e5e7eb' },
|
||||||
iconCircle: { width: 46, height: 46, borderRadius: 23, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff' },
|
typeCard: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
|
||||||
typeLabel: { flex: 1, fontSize: 17, fontWeight: '700', color: '#111827' },
|
iconCircle: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
|
||||||
backLink: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginTop: 2 },
|
typeLabel: { flex: 1, fontSize: 15, fontWeight: '600', color: '#111827' },
|
||||||
backLinkText: { fontSize: 14, fontWeight: '600', color: '#2563eb' },
|
backLink: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginHorizontal: 12, marginTop: 2, padding: 4 },
|
||||||
searchInput: { height: 48, paddingHorizontal: 14, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 12, backgroundColor: '#fff', fontSize: 16, color: '#111827' },
|
backLinkText: { fontSize: 14, fontWeight: '600', color: '#3d7a30' },
|
||||||
|
searchBox: { minHeight: 44, marginHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, backgroundColor: '#ffffff' },
|
||||||
|
searchInput: { flex: 1, paddingVertical: 10, fontSize: 15, color: '#111827' },
|
||||||
loader: { marginVertical: 32 },
|
loader: { marginVertical: 32 },
|
||||||
errorText: { padding: 16, borderRadius: 12, backgroundColor: '#fef2f2', color: '#b91c1c' },
|
errorText: { marginHorizontal: 16, padding: 12, borderRadius: 10, backgroundColor: '#fef2f2', color: '#b91c1c' },
|
||||||
emptyListText: { paddingVertical: 28, textAlign: 'center', color: '#6b7280' },
|
emptyListText: { paddingVertical: 28, textAlign: 'center', color: '#6b7280' },
|
||||||
targetList: { overflow: 'hidden', borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 14, backgroundColor: '#fff' },
|
targetList: { borderTopWidth: 1, borderTopColor: '#e5e7eb', backgroundColor: '#ffffff' },
|
||||||
targetRow: { minHeight: 64, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#d1d5db' },
|
targetRow: { minHeight: 64, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
|
||||||
targetName: { fontSize: 16, fontWeight: '600', color: '#111827' },
|
targetRowPressed: { backgroundColor: '#f3f4f6' },
|
||||||
|
targetName: { fontSize: 15, fontWeight: '600', color: '#111827' },
|
||||||
targetNumber: { marginTop: 3, fontSize: 13, color: '#6b7280' },
|
targetNumber: { marginTop: 3, fontSize: 13, color: '#6b7280' },
|
||||||
uploadOverlay: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 14, borderRadius: 12, backgroundColor: '#eff6ff' },
|
uploadIcon: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
|
||||||
uploadText: { fontWeight: '600', color: '#1e3a8a' },
|
uploadOverlay: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, marginHorizontal: 16, padding: 14, borderRadius: 10, backgroundColor: '#eff9ea' },
|
||||||
|
uploadText: { fontWeight: '600', color: '#2f5f24' },
|
||||||
cancelButton: { alignItems: 'center', padding: 14 },
|
cancelButton: { alignItems: 'center', padding: 14 },
|
||||||
cancelButtonText: { fontSize: 16, fontWeight: '600', color: '#b91c1c' },
|
cancelButtonText: { fontSize: 16, fontWeight: '600', color: '#b91c1c' },
|
||||||
emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 28 },
|
emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 28 },
|
||||||
|
emptyIcon: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
|
||||||
emptyTitle: { marginTop: 14, fontSize: 20, fontWeight: '700', color: '#111827' },
|
emptyTitle: { marginTop: 14, fontSize: 20, fontWeight: '700', color: '#111827' },
|
||||||
emptyText: { marginTop: 6, textAlign: 'center', color: '#6b7280' },
|
emptyText: { marginTop: 6, textAlign: 'center', color: '#6b7280' },
|
||||||
primaryButton: { marginTop: 22, paddingHorizontal: 18, paddingVertical: 13, borderRadius: 11, backgroundColor: '#2563eb' },
|
primaryButton: { marginTop: 22, paddingHorizontal: 18, paddingVertical: 13, borderRadius: 10, backgroundColor: PRIMARY },
|
||||||
primaryButtonText: { fontSize: 15, fontWeight: '700', color: '#fff' },
|
primaryButtonText: { fontSize: 15, fontWeight: '700', color: '#fff' },
|
||||||
});
|
});
|
||||||
|
|||||||
63
mobile/src/lib/resource-list-filters.ts
Normal file
63
mobile/src/lib/resource-list-filters.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Customer, Plant, Project } from './api';
|
||||||
|
|
||||||
|
function searchTerms(search: string): string[] {
|
||||||
|
return search.trim().toLocaleLowerCase('de').split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesTerms(values: unknown[], terms: string[]): boolean {
|
||||||
|
if (terms.length === 0) return true;
|
||||||
|
const haystack = values
|
||||||
|
.map((value) => String(value || '').toLocaleLowerCase('de'))
|
||||||
|
.join(' ');
|
||||||
|
return terms.every((term) => haystack.includes(term));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveProjectPhase(project: Project): string {
|
||||||
|
const explicit = String(project.active_phase || '').trim();
|
||||||
|
if (explicit) return explicit;
|
||||||
|
|
||||||
|
const phases = Array.isArray(project.phases) ? project.phases : [];
|
||||||
|
const active = phases.find((phase: any) => phase?.active);
|
||||||
|
return String(active?.label || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProjectCompleted(project: Project): boolean {
|
||||||
|
return getActiveProjectPhase(project).toLocaleLowerCase('de') === 'abgeschlossen';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterProjects(projects: Project[], search: string, showCompleted = false): Project[] {
|
||||||
|
const terms = searchTerms(search);
|
||||||
|
return projects.filter((project) => {
|
||||||
|
if (!showCompleted && isProjectCompleted(project)) return false;
|
||||||
|
return matchesTerms([
|
||||||
|
project.name,
|
||||||
|
project.projectNumber,
|
||||||
|
project.notes,
|
||||||
|
project.customerRef,
|
||||||
|
project.active_phase,
|
||||||
|
getActiveProjectPhase(project),
|
||||||
|
], terms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterCustomers(customers: Customer[], search: string, showArchived = false): Customer[] {
|
||||||
|
const terms = searchTerms(search);
|
||||||
|
return customers.filter((customer) => {
|
||||||
|
if (!showArchived && customer.archived) return false;
|
||||||
|
return matchesTerms([customer.name, customer.customerNumber, customer.notes], terms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlantCustomerName(raw: Plant['customer']): string | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (typeof raw === 'object') return raw.name ? String(raw.name) : null;
|
||||||
|
return String(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterPlants(plants: Plant[], search: string, showArchived = false): Plant[] {
|
||||||
|
const terms = searchTerms(search);
|
||||||
|
return plants.filter((plant) => {
|
||||||
|
if (!showArchived && plant.archived) return false;
|
||||||
|
return matchesTerms([plant.name, plant.description, getPlantCustomerName(plant.customer)], terms);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user