frontend changes
This commit is contained in:
parent
f0cc077b63
commit
952f260b5c
10
package-lock.json
generated
10
package-lock.json
generated
@ -12,7 +12,6 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
@ -948,15 +947,6 @@
|
||||
"fast-glob": "3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/font": {
|
||||
"version": "14.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@next/font/-/font-14.2.15.tgz",
|
||||
"integrity": "sha512-QopYhBmCDDrNDynbi+ZD1hDZXmQXVFo7TmAFp4DQgO/kogz1OLbQ92hPigJbj572eZ3GaaVxNIyYVn3/eAsehg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"next": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz",
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
|
||||
@ -11,6 +11,7 @@ import { ArrowLeft, Plus, Edit, Trash2 } from 'lucide-react'
|
||||
import { DatabaseTemplate, TemplateFeature } from '@/lib/template-service'
|
||||
import { AICustomFeatureCreator } from '@/components/ai/AICustomFeatureCreator'
|
||||
import { getApiUrl } from '@/config/backend'
|
||||
import { getAccessToken } from '@/components/apis/authApiClients'
|
||||
|
||||
interface AdminFeatureSelectionProps {
|
||||
template: {
|
||||
@ -31,17 +32,42 @@ interface AdminFeatureSelectionProps {
|
||||
export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectionProps) {
|
||||
// Admin template service functions using admin API endpoints
|
||||
const fetchFeatures = async (templateId: string): Promise<TemplateFeature[]> => {
|
||||
const response = await fetch(getApiUrl(`api/templates/${templateId}/features`))
|
||||
const token = getAccessToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl(`api/admin/templates/${templateId}/features`), {
|
||||
headers
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to fetch features')
|
||||
const data = await response.json()
|
||||
console.log('🔍 Raw API response for features:', data)
|
||||
// Handle different response structures
|
||||
return Array.isArray(data) ? data : (data.data || data.features || [])
|
||||
const features = Array.isArray(data) ? data : (data.data || data.features || [])
|
||||
console.log('🔍 Processed features with rules:', features.map((f: any) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
business_rules: f.business_rules,
|
||||
technical_requirements: f.technical_requirements
|
||||
})))
|
||||
return features
|
||||
}
|
||||
|
||||
// The features list already includes business_rules and technical_requirements
|
||||
// No need for separate API call since the data is already available
|
||||
|
||||
const createFeature = async (templateId: string, feature: Partial<TemplateFeature>) => {
|
||||
const token = getAccessToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl('api/features'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
body: JSON.stringify({ ...feature, template_id: templateId })
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to create feature')
|
||||
@ -49,26 +75,56 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
}
|
||||
|
||||
const updateFeature = async (templateId: string, featureId: string, feature: Partial<TemplateFeature>) => {
|
||||
const response = await fetch(getApiUrl(`api/templates/${templateId}/features/${featureId}`), {
|
||||
const token = getAccessToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
console.log('🔄 Updating feature:', { templateId, featureId, feature })
|
||||
console.log('📤 Sending data to backend:', JSON.stringify(feature, null, 2))
|
||||
|
||||
const response = await fetch(getApiUrl(`api/admin/templates/${templateId}/features/${featureId}`), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
body: JSON.stringify(feature)
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to update feature')
|
||||
return response.json()
|
||||
|
||||
const responseText = await response.text()
|
||||
console.log('📥 Backend response:', responseText)
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ Update failed:', response.status, responseText)
|
||||
throw new Error(`Failed to update feature: ${responseText}`)
|
||||
}
|
||||
|
||||
return JSON.parse(responseText)
|
||||
}
|
||||
|
||||
const deleteFeature = async (templateId: string, featureId: string) => {
|
||||
const response = await fetch(getApiUrl(`api/templates/${templateId}/features/${featureId}`), {
|
||||
method: 'DELETE'
|
||||
const token = getAccessToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl(`api/admin/templates/${templateId}/features/${featureId}`), {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to delete feature')
|
||||
}
|
||||
|
||||
const bulkCreateFeatures = async (templateId: string, features: Partial<TemplateFeature>[]) => {
|
||||
const response = await fetch(getApiUrl(`api/templates/${templateId}/features/bulk`), {
|
||||
const token = getAccessToken()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl(`api/admin/templates/${templateId}/features/bulk`), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
body: JSON.stringify({ features })
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to create features')
|
||||
@ -86,6 +142,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [showAIModal, setShowAIModal] = useState(false)
|
||||
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
|
||||
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(new Set())
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@ -205,6 +262,42 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
})
|
||||
}
|
||||
|
||||
const toggleDescription = (featureId: string) => {
|
||||
setExpandedDescriptions((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(featureId)) next.delete(featureId)
|
||||
else next.add(featureId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const TruncatedDescription = ({ feature }: { feature: TemplateFeature }) => {
|
||||
const isExpanded = expandedDescriptions.has(feature.id)
|
||||
const description = feature.description || 'No description provided.'
|
||||
const maxLength = 150
|
||||
const shouldTruncate = description.length > maxLength
|
||||
|
||||
if (!shouldTruncate) {
|
||||
return <p>{description}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{isExpanded ? description : `${description.substring(0, maxLength)}...`}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleDescription(feature.id)}
|
||||
className="p-0 h-auto text-orange-400 hover:text-orange-300 hover:bg-transparent mt-1"
|
||||
>
|
||||
{isExpanded ? 'Show less' : 'Show more'}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-20 text-white/60">Loading features...</div>
|
||||
}
|
||||
@ -239,7 +332,13 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-blue-500 text-blue-300 hover:bg-blue-500/10"
|
||||
onClick={(e) => { e.stopPropagation(); setEditingFeature(f) }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('🔍 Feature being edited:', f);
|
||||
console.log('🔍 Business rules:', f.business_rules);
|
||||
console.log('🔍 Technical requirements:', f.technical_requirements);
|
||||
setEditingFeature(f);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
@ -255,7 +354,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-white/80 text-sm space-y-2">
|
||||
<p>{f.description || 'No description provided.'}</p>
|
||||
<TruncatedDescription feature={f} />
|
||||
<div className="flex gap-2 text-xs">
|
||||
<Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
|
||||
<Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
|
||||
@ -271,11 +370,11 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-bold text-white">Select Features for {template.title}</h1>
|
||||
<p className="text-xl text-white/60 max-w-3xl mx-auto">Choose defaults or add your own essential features.</p>
|
||||
<p className="text-xl text-white/60 max-w-3xl mx-auto">add essential features.</p>
|
||||
</div>
|
||||
|
||||
{section('Essential Features', essentialFeatures)}
|
||||
{section('Suggested Features', suggestedFeatures)}
|
||||
{/* {section('Suggested Features', suggestedFeatures)} */}
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 space-y-3">
|
||||
<h3 className="text-white font-semibold">Add Essential Feature</h3>
|
||||
@ -297,15 +396,19 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
||||
projectType={template.type || template.title}
|
||||
editingFeature={editingFeature as any}
|
||||
onAdd={async (payload) => {
|
||||
await handleUpdate(editingFeature, {
|
||||
console.log('🔍 Payload from AICustomFeatureCreator:', payload);
|
||||
const updateData = {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
complexity: payload.complexity,
|
||||
// Map AI form structure to backend fields
|
||||
business_rules: payload.business_rules,
|
||||
// Flatten logic rules to store if backend supports it on update
|
||||
technical_requirements: payload.logic_rules,
|
||||
// Also send logic_rules for backward compatibility
|
||||
logic_rules: payload.logic_rules,
|
||||
} as any)
|
||||
};
|
||||
console.log('🔍 Update data being passed to handleUpdate:', updateData);
|
||||
await handleUpdate(editingFeature, updateData)
|
||||
setEditingFeature(null)
|
||||
}}
|
||||
onClose={() => setEditingFeature(null)}
|
||||
|
||||
@ -74,25 +74,40 @@ export function AICustomFeatureCreator({
|
||||
console.log('📋 Found business_rules field:', editingFeature.business_rules)
|
||||
businessRules = Array.isArray(editingFeature.business_rules)
|
||||
? editingFeature.business_rules
|
||||
: JSON.parse(editingFeature.business_rules)
|
||||
: (typeof editingFeature.business_rules === 'string' ? JSON.parse(editingFeature.business_rules) : editingFeature.business_rules)
|
||||
}
|
||||
// Then try additional_business_rules from feature_business_rules table
|
||||
else if ((editingFeature as any).additional_business_rules) {
|
||||
console.log('📋 Found additional_business_rules field:', (editingFeature as any).additional_business_rules)
|
||||
businessRules = Array.isArray((editingFeature as any).additional_business_rules)
|
||||
? (editingFeature as any).additional_business_rules
|
||||
: JSON.parse((editingFeature as any).additional_business_rules)
|
||||
: (typeof (editingFeature as any).additional_business_rules === 'string' ? JSON.parse((editingFeature as any).additional_business_rules) : (editingFeature as any).additional_business_rules)
|
||||
}
|
||||
// Also try technical_requirements field
|
||||
else if ((editingFeature as any).technical_requirements) {
|
||||
console.log('📋 Found technical_requirements field:', (editingFeature as any).technical_requirements)
|
||||
const techReqs = Array.isArray((editingFeature as any).technical_requirements)
|
||||
? (editingFeature as any).technical_requirements
|
||||
: (typeof (editingFeature as any).technical_requirements === 'string' ? JSON.parse((editingFeature as any).technical_requirements) : (editingFeature as any).technical_requirements)
|
||||
|
||||
// Convert technical requirements to business rules format
|
||||
if (Array.isArray(techReqs)) {
|
||||
businessRules = techReqs.map((req: string, index: number) => ({
|
||||
requirement: `Requirement ${index + 1}`,
|
||||
rules: [req]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📋 Parsed business rules:', businessRules)
|
||||
|
||||
if (businessRules && Array.isArray(businessRules) && businessRules.length > 0) {
|
||||
const requirements = businessRules.map((rule: any) => ({
|
||||
text: rule.requirement || rule.text || '',
|
||||
rules: rule.rules || []
|
||||
text: rule.requirement || rule.text || rule.name || `Requirement`,
|
||||
rules: Array.isArray(rule.rules) ? rule.rules : (rule.rules ? [rule.rules] : [])
|
||||
}))
|
||||
console.log('📋 Mapped requirements:', requirements)
|
||||
return requirements
|
||||
return requirements.length > 0 ? requirements : [{ text: '', rules: [] }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing business rules:', error)
|
||||
|
||||
@ -26,17 +26,26 @@ export const getAccessToken = () => {
|
||||
hasToken: !!token,
|
||||
tokenLength: token?.length || 0,
|
||||
tokenStart: token?.substring(0, 20) + '...' || 'No token',
|
||||
timestamp: new Date().toISOString(),
|
||||
localStorageToken: token,
|
||||
moduleToken: accessToken
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (token) {
|
||||
accessToken = token; // Update the module variable
|
||||
console.log('🔐 [getAccessToken] Updated module token from localStorage');
|
||||
return token; // Return the fresh token from localStorage
|
||||
// Validate token format (basic JWT structure check)
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
console.warn('🔐 [getAccessToken] Invalid token format, clearing...');
|
||||
clearTokens();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the module variable for consistency
|
||||
accessToken = token;
|
||||
console.log('🔐 [getAccessToken] Valid token found and updated');
|
||||
return token;
|
||||
} else {
|
||||
console.log('🔐 [getAccessToken] No token found in localStorage');
|
||||
return null; // Return null if no token found
|
||||
accessToken = null; // Ensure module variable is also null
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -84,9 +93,11 @@ export const authApiClient = axios.create({
|
||||
const addAuthTokenInterceptor = (client: typeof authApiClient) => {
|
||||
client.interceptors.request.use(
|
||||
(config) => {
|
||||
if (accessToken) {
|
||||
// Always get fresh token from localStorage instead of using module variable
|
||||
const freshToken = getAccessToken();
|
||||
if (freshToken) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
config.headers.Authorization = `Bearer ${freshToken}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
@ -104,9 +115,11 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
||||
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
if (refreshToken) {
|
||||
// Always get fresh refresh token from localStorage
|
||||
const freshRefreshToken = getRefreshToken();
|
||||
if (freshRefreshToken) {
|
||||
const response = await client.post('/api/auth/refresh', {
|
||||
refreshToken: refreshToken
|
||||
refreshToken: freshRefreshToken
|
||||
});
|
||||
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data.data.tokens;
|
||||
setTokens(newAccessToken, newRefreshToken);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// Main backend URL - change this to update all API calls
|
||||
export const BACKEND_URL = 'http://192.168.1.11:8000';
|
||||
export const BACKEND_URL = 'http://192.168.1.25:8000';
|
||||
// export const BACKEND_URL = 'https://backend.codenuk.com';
|
||||
|
||||
|
||||
|
||||
@ -350,7 +350,7 @@ export const adminApi = {
|
||||
technical_requirements?: Record<string, unknown>;
|
||||
feature_type?: string;
|
||||
}): Promise<AdminFeature> => {
|
||||
const response = await apiCall<AdminFeature>(`/api/templates/${templateId}/features/${featureId}`, {
|
||||
const response = await apiCall<AdminFeature>(`/api/templates/${templateId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
@ -36,6 +36,8 @@ export interface TemplateFeature {
|
||||
created_by_user: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
business_rules?: any
|
||||
technical_requirements?: any
|
||||
}
|
||||
|
||||
export type CreateFeaturePayload = Partial<TemplateFeature> & {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user