modules/frontend/Write-Frontend-RN-Feature.ps1

param(
    [string]$ProjectName,
    [string]$OutputPath,
    [string]$Platform,
    [string[]]$Entities,
    [string]$Language = "tr"
)

. (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1")
. (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Get-Locale.ps1")
$L = Get-Locale -Language $Language

$MobilePath = Join-Path $OutputPath "$ProjectName-mobile"
$AppPath = Join-Path $MobilePath "app"
$HooksPath = Join-Path $MobilePath "hooks"
$ServicesPath = Join-Path $MobilePath "services"

if (-not $Global:DryRun) {
    New-Item -ItemType Directory -Path $HooksPath -Force | Out-Null
    New-Item -ItemType Directory -Path $ServicesPath -Force | Out-Null
}

$nonUserEntities = $Entities | Where-Object { $_ -ne "User" }

# 1. Write services/api.ts (Api Client instance)
$apiTs = @'
import { Platform } from 'react-native';
 
export class ApiClient {
  private baseUrl: string;
  private token: string | null = null;
 
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }
 
  setToken(token: string | null) {
    this.token = token;
  }
 
  async request(endpoint: string, options: RequestInit = {}) {
    const headers = new Headers(options.headers || {});
    if (this.token) {
      headers.set('Authorization', `Bearer ${this.token}`);
    }
    if (!headers.has('Content-Type') && options.body) {
      headers.set('Content-Type', 'application/json');
    }
 
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`, {
        ...options,
        headers,
      });
 
      if (!response.ok) {
        const errText = await response.text();
        throw new Error(errText || response.statusText);
      }
 
      if (response.status === 204) {
        return null;
      }
 
      return response.json();
    } catch (err: any) {
      console.error(`API Request Failed [${this.baseUrl}${endpoint}]:`, err.message || err);
      throw err;
    }
  }
}
 
const getDefaultApiUrl = () => {
  let url = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8080/api';
  if (Platform.OS === 'android' && url.includes('localhost')) {
    url = url.replace('localhost', '10.0.2.2');
  }
  return url;
};
 
export const api = new ApiClient(getDefaultApiUrl());
'@

Write-CoreFile (Join-Path $ServicesPath "api.ts") $apiTs

# 2. Write React Query hooks and screen components for each entity
foreach ($e in $nonUserEntities) {
    $lowE = $e.ToLower()
    $pE = Get-Plural $e
    $lowPE = $pE.ToLower()

    # Create entity directories
    if (-not $Global:DryRun) {
        New-Item -ItemType Directory -Path (Join-Path $AppPath "(tabs)\$lowPE") -Force | Out-Null
    }

    # Feature Stack Layout (app/(tabs)/$lowPE/_layout.tsx)
    $featureLayout = @'
import { Stack } from 'expo-router';
 
export default function FeatureLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="index" />
      <Stack.Screen name="add" />
    </Stack>
  );
}
'@

    Write-CoreFile (Join-Path $AppPath "(tabs)\$lowPE\_layout.tsx") $featureLayout

    # Hooks file
    $hookContent = @'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../services/api';
 
const KEY = ['__LOWPE__'];
 
export function use__PE__(page: number = 1, size: number = 20) {
  return useQuery({
    queryKey: [...KEY, page, size],
    queryFn: () => api.request(`/__LOWPE__/paged?page=${page}&size=${size}`),
  });
}
 
export function useAdd__E__() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (vars: { name: string; price?: number }) => api.request('/__LOWPE__', {
      method: 'POST',
      body: JSON.stringify(vars)
    }),
    onSuccess: () => qc.invalidateQueries({ queryKey: KEY }),
  });
}
'@
.Replace('__LOWPE__', $lowPE).Replace('__PE__', $pE).Replace('__E__', $e)
    Write-CoreFile (Join-Path $HooksPath "use$pE.ts") $hookContent

    # Entity Screen (FlatList index.tsx)
    $priceCell = if ($e -eq "Product") { "`n <Text style={styles.itemSub}>{item.price !== undefined ? ``\`\`${item.price} TL\`\` : ''}</Text>" } else { "" }
    $indexScreen = @'
import { View, Text, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useRouter } from 'expo-router';
import { use__PE__ } from '../../../hooks/use__PE__';
import { useState } from 'react';
 
export default function __PE__Screen() {
  const router = useRouter();
  const [page, setPage] = useState(1);
  const { data, isLoading, isError } = use__PE__(page, 20);
   
  const items = data?.data?.items || [];
  const totalPages = data?.data?.totalPages || 1;
 
  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.addButton} onPress={() => router.push('/__LOWPE__/add')}>
        <Text style={styles.addButtonText}>+ __UI_NEW_ENTITY__</Text>
      </TouchableOpacity>
 
      {isLoading ? (
        <ActivityIndicator size="large" color="#3b82f6" style={{ marginTop: 40 }} />
      ) : (
        <FlatList
          data={items}
          keyExtractor={(item: any) => item.id.toString()}
          style={styles.list}
          renderItem={({ item }: { item: any }) => (
            <View style={styles.itemCard}>
              <Text style={styles.itemText}>{item.name}</Text>__PRICE_CELL__
            </View>
          )}
          ListEmptyComponent={
            <Text style={styles.emptyText}>__UI_NO_DATA__</Text>
          }
        />
      )}
 
      <View style={styles.pagination}>
        <TouchableOpacity
          style={[styles.pageButton, page === 1 && styles.disabled]}
          disabled={page === 1}
          onPress={() => setPage(p => Math.max(1, p - 1))}
        >
          <Text style={styles.pageButtonText}>__UI_PREVIOUS__</Text>
        </TouchableOpacity>
         
        <Text style={styles.pageInfo}>__UI_PAGE__</Text>
 
        <TouchableOpacity
          style={[styles.pageButton, page >= totalPages && styles.disabled]}
          disabled={page >= totalPages}
          onPress={() => setPage(p => Math.min(totalPages, p + 1))}
        >
          <Text style={styles.pageButtonText}>__UI_NEXT__</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#121212',
    padding: 16,
  },
  addButton: {
    backgroundColor: '#3b82f6',
    padding: 14,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 16,
  },
  addButtonText: {
    color: '#ffffff',
    fontWeight: 'bold',
    fontSize: 16,
  },
  list: {
    flex: 1,
  },
  itemCard: {
    backgroundColor: '#1e1e1e',
    padding: 16,
    borderRadius: 8,
    marginBottom: 10,
    borderWidth: 1,
    borderColor: '#333333',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  itemText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '500',
  },
  itemSub: {
    color: '#10b981',
    fontWeight: '600',
  },
  emptyText: {
    color: '#888888',
    textAlign: 'center',
    marginTop: 40,
    fontSize: 15,
  },
  pagination: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginTop: 10,
  },
  pageButton: {
    backgroundColor: '#1e1e1e',
    paddingVertical: 10,
    paddingHorizontal: 16,
    borderRadius: 6,
  },
  pageButtonText: {
    color: '#ffffff',
    fontWeight: '600',
  },
  disabled: {
    opacity: 0.5,
  },
  pageInfo: {
    color: '#888888',
  },
});
'@
.Replace('__PE__', $pE).Replace('__LOWPE__', $lowPE).Replace('__UI_NEW_ENTITY__', ($L.uiNewEntity -f $e)).Replace('__UI_NO_DATA__', $L.uiNoData).Replace('__UI_PREVIOUS__', $L.uiPrevious).Replace('__UI_PAGE__', ($L.uiPage -f '{page}', '{totalPages}')).Replace('__UI_NEXT__', $L.uiNext).Replace('__PRICE_CELL__', $priceCell)
    Write-CoreFile (Join-Path $AppPath "(tabs)\$lowPE\index.tsx") $indexScreen

    # Entity Add Screen (add.tsx)
    $priceFieldState = if ($e -eq "Product") { " const [price, setPrice] = useState('');" } else { "" }
    $priceInputMarkup = if ($e -eq "Product") { @"
      <Text style={styles.label}>$($L.uiPrice)</Text>
      <TextInput
        style={styles.input}
        value={price}
        onChangeText={setPrice}
        keyboardType="numeric"
        placeholder="0.00"
        placeholderTextColor="#666666"
      />
"@
 } else { "" }
    $mutatePayload = if ($e -eq "Product") { "{ name, price: parseFloat(price) || 0 }" } else { "{ name }" }

    $addScreen = @"
import { View, Text, TextInput, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { useAdd$e } from '../../../hooks/use$pE';
 
export default function Add${e}Screen() {
  const router = useRouter();
  const [name, setName] = useState('');
$priceFieldState
  const addMutation = useAdd$e();
 
  const handleSubmit = async () => {
    if (!name.trim()) {
      Alert.alert('Error', 'Name is required.');
      return;
    }
    try {
      await addMutation.mutateAsync($mutatePayload);
      Alert.alert('Success', '$($L.uiEntityAdded -f $e)');
      router.back();
    } catch (err: any) {
      Alert.alert('Error', err.message || '$($L.uiAddFailed)');
    }
  };
 
  return (
    <View style={styles.container}>
      <Text style={styles.label}>$($L.uiEntityName -f $e)</Text>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={setName}
        placeholder="Name"
        placeholderTextColor="#666666"
      />
 
$priceInputMarkup
      <TouchableOpacity
        style={[styles.saveButton, addMutation.isPending && styles.disabled]}
        onPress={handleSubmit}
        disabled={addMutation.isPending}
      >
        {addMutation.isPending ? (
          <ActivityIndicator color="#ffffff" />
        ) : (
          <Text style={styles.saveButtonText}>$($L.uiAdd)</Text>
        )}
      </TouchableOpacity>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#121212',
    padding: 20,
  },
  label: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
    marginTop: 12,
  },
  input: {
    backgroundColor: '#1e1e1e',
    color: '#ffffff',
    padding: 14,
    borderRadius: 8,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#333333',
  },
  saveButton: {
    backgroundColor: '#3b82f6',
    padding: 14,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 30,
  },
  saveButtonText: {
    color: '#ffffff',
    fontWeight: 'bold',
    fontSize: 16,
  },
  disabled: {
    opacity: 0.7,
  },
});
"@

    Write-CoreFile (Join-Path $AppPath "(tabs)\$lowPE\add.tsx") $addScreen
}