feat(boc): Complete Business Operations Center v1.0

- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
This commit is contained in:
Bernt
2026-07-12 12:41:35 +00:00
parent 4789a7fb48
commit 58ca4e68db
26666 changed files with 575891 additions and 2074516 deletions
+1
View File
@@ -28,6 +28,7 @@
"react-native-maps": "^1.27.2",
"react-native-safe-area-context": "^5.7.0",
"react-native-screens": "~4.23.0",
"react-native-webview": "^13.12.0",
"zustand": "^5.0.12"
},
"devDependencies": {
@@ -16,6 +16,7 @@ import { MissionsScreen } from '../../screens/MissionsScreen'
import { MissionDetailScreen } from '../../screens/MissionDetailScreen'
import { EarningsScreen } from '../../screens/EarningsScreen'
import { ProfileScreen } from '../../screens/ProfileScreen'
import { ShopScreen } from '../../screens/ShopScreen'
import { AuthScreen } from '../features/auth/AuthScreen'
import { PasswordlessApprovalScreen } from '../features/auth/PasswordlessApprovalScreen'
import { usePasswordlessAuth } from '../features/auth/usePasswordlessAuth'
@@ -67,6 +68,13 @@ function MainTabs() {
tabBarIcon: ({ color }) => <Text style={{ fontSize: 20, color }}>💰</Text>,
}}
/>
<Tab.Screen
name="Shop"
component={ShopScreen}
options={{
tabBarIcon: ({ color }) => <Text style={{ fontSize: 20, color }}>🛒</Text>,
}}
/>
<Tab.Screen
name="Profil"
component={ProfileScreen}
+338
View File
@@ -0,0 +1,338 @@
/**
* ShopScreen — quiXzoom Field Gear Collection
* Webview som laddar shoppen från quixzoom-shop servicen
*
* quiXzoom Field Program:
* - 09 uppdrag: Contributor (digital profil)
* - 10+ uppdrag: Field Contributor (låser upp Field Gear Store)
* - 100+ uppdrag med 95% godkännandegrad: Verified Field Contributor (låser upp Field ID Kit)
*/
import { useState, useEffect } from 'react'
import {
View,
Text,
StyleSheet,
ActivityIndicator,
Alert,
TouchableOpacity,
SafeAreaView,
} from 'react-native'
import { WebView } from 'react-native-webview'
import { getToken } from '../../lib/api'
const SHOP_URL = 'https://shop.quixzoom.com'
// const SHOP_URL = 'http://localhost:8087' // för utveckling
type UserTier = 'contributor' | 'field_contributor' | 'verified_field_contributor'
interface TierData {
tier: UserTier
completed_missions: number
approval_rate: number
identity_verified: boolean
serious_violations: number
unlocked_features: string[]
}
export function ShopScreen() {
const [loading, setLoading] = useState(true)
const [tier, setTier] = useState<UserTier>('contributor')
const [tierData, setTierData] = useState<TierData | null>(null)
const [token, setToken] = useState<string | null>(null)
useEffect(() => {
checkTier()
}, [])
const checkTier = async () => {
try {
const t = await getToken()
setToken(t)
if (!t) {
setLoading(false)
return
}
const res = await fetch(`${SHOP_URL}/api/tier`, {
headers: { Authorization: `Bearer ${t}` },
})
if (!res.ok) throw new Error('Kunde inte hämta status')
const data: TierData = await res.json()
setTierData(data)
setTier(data.tier)
} catch (err) {
console.error('[ShopScreen] Tier check failed:', err)
} finally {
setLoading(false)
}
}
const injectedJavaScript = `
(function() {
const token = '${token || ''}';
if (token) {
localStorage.setItem('quixzoom_token', token);
}
window.ReactNativeWebView?.postMessage('loaded');
})();
true;
`
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.center}>
<ActivityIndicator size="large" color="#6366f1" />
<Text style={styles.loadingText}>Laddar shoppen...</Text>
</View>
</SafeAreaView>
)
}
if (!token) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.center}>
<Text style={styles.emoji}>🔒</Text>
<Text style={styles.title}>Logga in först</Text>
<Text style={styles.subtitle}>
Du måste vara inloggad för att se shoppen
</Text>
</View>
</SafeAreaView>
)
}
// Contributor (0-9 uppdrag) — låst
if (tier === 'contributor') {
const missions = tierData?.completed_missions || 0
const remaining = Math.max(0, 10 - missions)
return (
<SafeAreaView style={styles.container}>
<View style={styles.center}>
<Text style={styles.emoji}>👤</Text>
<Text style={styles.tierLabel}>Contributor</Text>
<Text style={styles.title}>Field Gear Store är låst</Text>
<Text style={styles.subtitle}>
Slutför {remaining} uppdrag till för att låsa upp shoppen
</Text>
<View style={styles.progressContainer}>
<View style={[styles.progressBar, { width: `${Math.min((missions / 10) * 100, 100)}%` }]} />
</View>
<Text style={styles.progressText}>
{missions} / 10 uppdrag
</Text>
<View style={styles.benefitsBox}>
<Text style={styles.benefitsTitle}>Dina nuvarande förmåner:</Text>
<Text style={styles.benefitItem}> Digital profil i appen</Text>
<Text style={styles.benefitItem}> Digital verifiering</Text>
</View>
<TouchableOpacity style={styles.button} onPress={checkTier}>
<Text style={styles.buttonText}>Uppdatera</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
)
}
// Field Contributor (10+ uppdrag) — shoppen upplåst
if (tier === 'field_contributor') {
const missions = tierData?.completed_missions || 0
const toVerified = Math.max(0, 100 - missions)
return (
<SafeAreaView style={styles.container}>
<View style={styles.tierBanner}>
<Text style={styles.tierBannerText}>🎯 Field Contributor</Text>
<Text style={styles.tierBannerSub}>
{toVerified} uppdrag till Verified Field Contributor
</Text>
</View>
<WebView
source={{ uri: SHOP_URL }}
injectedJavaScript={injectedJavaScript}
onMessage={(event) => {
console.log('[ShopScreen] WebView message:', event.nativeEvent.data)
}}
onError={(err) => {
console.error('[ShopScreen] WebView error:', err)
Alert.alert('Fel', 'Kunde inte ladda shoppen')
}}
startInLoadingState={true}
renderLoading={() => (
<View style={styles.webviewLoading}>
<ActivityIndicator size="large" color="#6366f1" />
</View>
)}
/>
</SafeAreaView>
)
}
// Verified Field Contributor (100+ uppdrag, 95% approval) — allt upplåst
return (
<SafeAreaView style={styles.container}>
<View style={styles.tierBannerVerified}>
<Text style={styles.tierBannerTextVerified}> Verified Field Contributor</Text>
<Text style={styles.tierBannerSubVerified}>
{tierData?.completed_missions} uppdrag {(tierData?.approval_rate || 0 * 100).toFixed(0)}% godkänt
</Text>
</View>
<WebView
source={{ uri: SHOP_URL }}
injectedJavaScript={injectedJavaScript}
onMessage={(event) => {
console.log('[ShopScreen] WebView message:', event.nativeEvent.data)
}}
onError={(err) => {
console.error('[ShopScreen] WebView error:', err)
Alert.alert('Fel', 'Kunde inte ladda shoppen')
}}
startInLoadingState={true}
renderLoading={() => (
<View style={styles.webviewLoading}>
<ActivityIndicator size="large" color="#6366f1" />
</View>
)}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0A0A1B',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
loadingText: {
color: 'rgba(255,255,255,0.6)',
marginTop: 16,
fontSize: 16,
},
emoji: {
fontSize: 64,
marginBottom: 16,
},
tierLabel: {
fontSize: 14,
color: 'rgba(255,255,255,0.5)',
marginBottom: 8,
textTransform: 'uppercase',
letterSpacing: 2,
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#fff',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
color: 'rgba(255,255,255,0.6)',
textAlign: 'center',
marginBottom: 24,
},
progressContainer: {
width: '80%',
height: 8,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 4,
marginBottom: 8,
overflow: 'hidden',
},
progressBar: {
height: '100%',
backgroundColor: '#6366f1',
borderRadius: 4,
},
progressText: {
color: 'rgba(255,255,255,0.5)',
fontSize: 14,
marginBottom: 24,
},
benefitsBox: {
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 12,
padding: 16,
width: '100%',
marginBottom: 24,
},
benefitsTitle: {
color: '#fff',
fontWeight: '600',
marginBottom: 8,
},
benefitItem: {
color: 'rgba(255,255,255,0.6)',
fontSize: 14,
marginBottom: 4,
},
button: {
backgroundColor: '#6366f1',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
tierBanner: {
backgroundColor: 'rgba(99, 102, 241, 0.15)',
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: 'rgba(99, 102, 241, 0.3)',
},
tierBannerText: {
color: '#6366f1',
fontWeight: '700',
fontSize: 14,
},
tierBannerSub: {
color: 'rgba(255,255,255,0.5)',
fontSize: 12,
marginTop: 2,
},
tierBannerVerified: {
backgroundColor: 'rgba(139, 92, 246, 0.15)',
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: 'rgba(139, 92, 246, 0.3)',
},
tierBannerTextVerified: {
color: '#8b5cf6',
fontWeight: '700',
fontSize: 14,
},
tierBannerSubVerified: {
color: 'rgba(255,255,255,0.5)',
fontSize: 12,
marginTop: 2,
},
webviewLoading: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0A0A1B',
},
})