modules/ai-guide/Write-AiGuide-Frontend.ps1

param(
    [string]   $ProjectName,
    [string]   $OrmMode,
    [string]   $DbProvider    = "MSSQL",
    [string]   $FrontendMode,
    [string[]] $Entities      = @("User", "Product"),
    [string]   $OutputPath,
    [bool]     $SkipTests     = $false,
    [bool]     $SkipCi        = $false,
    [bool]     $SkipFrontend  = $false,
    [bool]     $Observability = $false,
    [string]   $IdeConfig     = "VSCode",
    [bool]     $I18n          = $false,
    [string]   $Platform      = "Web"
)

. (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1")

$guideDir = Join-Path $OutputPath "ai-guide"
function Write-Guide($fileName, $content) {
    Write-CoreFile (Join-Path $guideDir $fileName) $content
}

$nonAuthEntities = @($Entities | Where-Object { $_ -ne "User" })
$firstE  = if ($nonAuthEntities.Count -gt 0) { $nonAuthEntities[0] } else { "Product" }
$firstPE = Get-Plural $firstE
$firstLowE = $firstE.ToLower()

# ─── testing-guide.md ─────────────────────────────────────
if (-not $SkipTests) {
    Write-Guide "testing-guide.md" @"
# Test Yazma Rehberi
 
Projede 3 test katmani bulunur: **Domain.Tests**, **Application.Tests**, **Integration.Tests**
 
## Test Calistirma
 
``````bash
dotnet test # Tum testler
dotnet test --filter "Category=Unit" # Sadece unit testler
dotnet test --logger "console;verbosity=normal"
``````
 
---
 
## Domain.Tests — Saf Unit Test
 
Domain entity'lerinin davranisini test eder. Dis bagimlilik yoktur.
 
``````csharp
// tests/$ProjectName.Domain.Tests/Entities/${firstE}Tests.cs
public class ${firstE}Tests
{
    [Fact]
    public void New${firstE}_ShouldHave_DefaultValues()
    {
        var entity = new $firstE { Name = "Test" };
 
        entity.IsDeleted.Should().BeFalse();
        entity.DeletedAt.Should().BeNull();
        entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
    }
 
    [Fact]
    public void SoftDelete_ShouldSet_IsDeletedFlag()
    {
        var entity = new $firstE { Name = "Test" };
        entity.IsDeleted = true;
        entity.DeletedAt = DateTime.UtcNow;
 
        entity.IsDeleted.Should().BeTrue();
        entity.DeletedAt.Should().NotBeNull();
    }
}
``````
 
---
 
## Application.Tests — Handler Unit Test
 
Handler'lari mock repository ile test eder.
 
``````csharp
// tests/$ProjectName.Application.Tests/Features/${firstE}s/GetAll${firstE}sQueryHandlerTests.cs
public class GetAll${firstE}sQueryHandlerTests
{
    private readonly Mock<I${firstE}Repository> _repoMock = new();
    private readonly Mock<IMapper> _mapperMock = new();
 
    [Fact]
    public async Task Handle_ShouldReturn_MappedDtos_WhenEntitiesExist()
    {
        // Arrange
        var entities = new List<$firstE> { new() { Id = 1, Name = "Test" } };
        var dtos = new List<${firstE}Dto> { new(1, "Test", DateTime.UtcNow) };
 
        _repoMock.Setup(r => r.GetAllAsync()).ReturnsAsync(entities);
        _mapperMock.Setup(m => m.Map<IEnumerable<${firstE}Dto>>(entities)).Returns(dtos);
 
        var handler = new GetAll${firstE}sQueryHandler(_repoMock.Object, _mapperMock.Object);
 
        // Act
        var result = await handler.Handle(new GetAll${firstE}sQuery(), CancellationToken.None);
 
        // Assert
        result.Should().HaveCount(1);
        result.First().Id.Should().Be(1);
        _repoMock.Verify(r => r.GetAllAsync(), Times.Once);
    }
 
    [Fact]
    public async Task Handle_GetPaged_ShouldReturn_PagedResult()
    {
        // Arrange
        var entities = new List<$firstE> { new() { Id = 1, Name = "Test" } };
        var pagedResult = new PagedResult<$firstE> { Items = entities, TotalCount = 1, Page = 1, Size = 20 };
        _repoMock.Setup(r => r.GetPagedAsync(It.IsAny<PagedQuery>())).ReturnsAsync(pagedResult);
        _mapperMock.Setup(m => m.Map<IEnumerable<${firstE}Dto>>(entities)).Returns(new List<${firstE}Dto>());
 
        var handler = new GetPaged${firstE}sQueryHandler(_repoMock.Object, _mapperMock.Object);
 
        // Act
        var result = await handler.Handle(new GetPaged${firstE}sQuery(1, 20), CancellationToken.None);
 
        // Assert
        result.TotalCount.Should().Be(1);
        result.Page.Should().Be(1);
    }
}
``````
 
---
 
## Validation Tests
 
``````csharp
public class Add${firstE}CommandValidatorTests
{
    private readonly Add${firstE}CommandValidator _validator = new();
 
    [Fact]
    public void Validate_ShouldFail_WhenNameIsEmpty()
    {
        var cmd = new Add${firstE}Command("");
        var result = _validator.TestValidate(cmd);
        result.ShouldHaveValidationErrorFor(x => x.Name);
    }
 
    [Fact]
    public void Validate_ShouldPass_WhenNameIsValid()
    {
        var cmd = new Add${firstE}Command("Valid Name");
        var result = _validator.TestValidate(cmd);
        result.ShouldNotHaveAnyValidationErrors();
    }
}
``````
 
---
 
## Integration.Tests — API Endpoint Test
 
``````csharp
// tests/$ProjectName.Integration.Tests/${firstE}EndpointTests.cs
public class ${firstE}EndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
 
    public ${firstE}EndpointTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }
 
    private async Task AuthenticateAsync() {
        var login = await _client.PostAsJsonAsync("/api/auth/login", new { username = "admin", password = "Admin123!" });
        var body = await login.Content.ReadFromJsonAsync<JsonElement>();
        var token = body.GetProperty("data").GetProperty("token").GetString();
        _client.DefaultRequestHeaders.Authorization = new("Bearer", token);
    }
 
    [Fact]
    public async Task GetPaged_ShouldReturn200_WithPagedResult()
    {
        await AuthenticateAsync();
        var response = await _client.GetAsync("/api/${firstLowE}s/paged?page=1&size=10");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var body = await response.Content.ReadFromJsonAsync<JsonElement>();
        body.GetProperty("success").GetBoolean().Should().BeTrue();
    }
 
    [Fact]
    public async Task GetPaged_ShouldReturn401_WithoutToken()
    {
        var response = await _client.GetAsync("/api/${firstLowE}s/paged");
        response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }
}
``````
"@

}

# ─── ci-cd.md ─────────────────────────────────────────────
if (-not $SkipCi) {
    Write-Guide "ci-cd.md" @"
# CI/CD Pipeline Kilavuzu
 
Proje iki GitHub Actions workflow ile geliyor:
 
- ``.github/workflows/ci.yml`` — Build + Test
- ``.github/workflows/docker.yml`` — Docker image build + push
 
---
 
## ci.yml - Build ve Test
 
Her ``push`` ve ``pull_request``'te tetiklenir:
 
``````yaml
# Adimlar:
# 1. dotnet restore
# 2. dotnet build --no-restore
# 3. dotnet test --no-build --verbosity normal
``````
 
**Local test:**
``````bash
dotnet restore
dotnet build --no-restore
dotnet test --no-build
``````
 
---
 
## docker.yml - Docker Build
 
``main`` branch'e push edildiginde tetiklenir:
 
``````yaml
# Adimlar:
# 1. Docker Buildx kur
# 2. Docker Hub'a login (secrets gerekli)
# 3. API image build + push
``````
 
**Gerekli GitHub Secrets:**
| Secret | Aciklama |
|--------|----------|
| ``DOCKER_USERNAME`` | Docker Hub kullanici adi |
| ``DOCKER_PASSWORD`` | Docker Hub sifre/token |
 
---
 
## Local Docker Calistirma
 
``````bash
# Tum stack (API + DB + UI)
docker-compose up --build
 
# Sadece API + DB
docker-compose up api db
 
# Loglar
docker-compose logs -f api
``````
 
---
 
$(if ($OrmMode -ne "Dapper") { @"
---
 
## Migration Yonetimi
 
Projede hazir migration script'leri bulunur:
 
``````powershell
# Windows - Yeni migration ekle
.\scripts\migrate.ps1 -MigrationName "AddOrderTable"
 
# Windows - Son migration'i geri al
.\scripts\migrate.ps1 -Revert
``````
 
``````bash
# Linux/Mac
./scripts/migrate.sh AddOrderTable
./scripts/migrate.sh --revert
``````
 
"@ })---
 
## Ortam Degiskenleri
 
| Degisken | Kapsam | Aciklama |
|----------|--------|----------|
| ``ConnectionStrings__DefaultConnection`` | API | Veritabani baglanti dizesi |
| ``JwtSettings__Secret`` | API | JWT imzalama anahtari (min 32 char) |
| ``JwtSettings__ExpiryMinutes`` | API | Token gecerlilik suresi |
| ``VITE_API_URL`` | Frontend | Backend API URL (Vite build-time) |
| ``NEXT_PUBLIC_API_URL`` | Frontend | Backend API URL (Next.js build-time) |
"@

}

# ─── observability.md ─────────────────────────────────────
if ($Observability) {
    Write-Guide "observability.md" @"
# Observability Kilavuzu - Serilog + OpenTelemetry
 
## Serilog (Structured Logging)
 
Tum loglar yapısal formatta yazilir. Gelistirme: console + Seq. Production: dosya veya cloud sink.
 
``````csharp
// Handler icinde log kullanimi:
_logger.LogInformation("${firstE} eklendi. Id={Id}, Name={Name}", entity.Id, entity.Name);
_logger.LogWarning("${firstE} bulunamadi. Id={Id}", id);
_logger.LogError(ex, "${firstE} eklenirken hata olustu.");
``````
 
**Seq UI:** http://localhost:5341 (docker-compose.override.yml ile)
 
---
 
## OpenTelemetry (Distributed Tracing)
 
Her HTTP istek otomatik trace edilir. Span'lar Jaeger/Zipkin/OTLP'ye gonderilebilir.
 
``````csharp
// Ek span eklemek icin:
using var activity = Activity.Current?.Source.StartActivity("CustomOperation");
activity?.SetTag("entity.id", id);
activity?.SetTag("entity.type", "$firstE");
``````
 
---
 
## Metrikler
 
| Metrik | Aciklama |
|--------|----------|
| ``http.server.request.duration`` | HTTP istek suresi |
| ``http.server.active_requests`` | Aktif istek sayisi |
 
**Dashboard:** Grafana + Prometheus ile izlenebilir.
"@

}

# ─── frontend-guide.md ────────────────────────────────────
if (-not $SkipFrontend) {
    $isNextJs = ($FrontendMode -eq "NextJS")
    $isTS     = ($FrontendMode -ne "ReactJS")

    $apiBaseNote = if ($isNextJs) { "``process.env.NEXT_PUBLIC_API_URL``" } else { "``import.meta.env.VITE_API_URL``" }
    $routerNote  = if ($isNextJs) { "Next.js App Router (``app/`` klasoru). Her klasor bir route." } else { "React Router v6 (``BrowserRouter + Routes``). ``App.tsx``'de tanimli." }
    $i18nSection = if ($I18n) { @"
 
## i18n (Cok Dil Destegi)
 
``````typescript
import { useTranslation } from 'react-i18next';
 
export function MyComponent() {
  const { t, i18n } = useTranslation();
  return (
    <div>
      <p>{t('app.brand')}</p>
      <button onClick={() => i18n.changeLanguage('tr')}>TR</button>
      <button onClick={() => i18n.changeLanguage('en')}>EN</button>
    </div>
  );
}
``````
 
Hazir ``LanguageSwitcher`` bileseni ``components/common/LanguageSwitcher.tsx``'de uretilir:
``````typescript
import { LanguageSwitcher } from '../components/common/LanguageSwitcher';
// Navbar veya App.tsx icine ekle:
<LanguageSwitcher />
``````
 
Dil dosyalari: ``public/locales/en/common.json`` ve ``public/locales/tr/common.json``
 
Yeni anahtar eklemek icin her iki dosyaya da ekle:
``````json
{ "myKey": "My Value" } // en/common.json
{ "myKey": "Degerim" } // tr/common.json
``````
"@
 } else { "" }

    $webDir = if ($Platform -eq "Both") { "apps/web" } else { "frontend" }
    Write-Guide "frontend-guide.md" @"
# Frontend Gelistirici Kilavuzu
 
Framework: **$FrontendMode**
 
---
 
## Klasor Yapisi
 
```
$webDir/
+-- $(if ($isNextJs) { 'app/ <- Next.js App Router sayfalar' } else { 'pages/ <- Sayfa bileşenleri (LoginPage, {E}PageV2)' })
+-- components/
| +-- auth/ <- LoginForm
| +-- common/ <- Button, Input, Alert
| +-- {entity}s/ <- {Entity}Form, {Entity}Table
+-- context/
| +-- AuthContext <- Kimlik dogrulama state
+-- hooks/
| +-- use{Entity}s <- React Query hooks (liste + mutasyon)
+-- services/
| +-- authService <- Login/logout/token yonetimi
| +-- {e}Service <- API cagrisi (getAll, getPaged, add, remove)
+-- types/
| +-- index.ts <- Interface tanimlari
```
 
---
 
## Kimlik Dogrulama Akisi
 
``````
1. LoginPage -> authService.login({ username, password })
2. API: POST /api/auth/login -> { success: true, data: { token, username } }
3. Token localStorage'a kaydedilir
4. AuthContext.login(token, username) -> isAuthenticated: true
5. PrivateRoute -> korunanli sayfalara erisim
6. 401 gelirse -> logout() + navigate('/login')
``````
 
``````typescript
// AuthContext kullanimi:
const { username, isAuthenticated, login, logout } = useAuth();
``````
 
---
 
## React Query Kurulumu
 
``QueryClientProvider`` ``main.tsx``'e otomatik eklenir. Varsayilan konfigurasyon:
 
``````typescript
const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
});
 
// main.tsx:
<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>
``````
 
---
 
## React Query Hooks
 
Her entity icin hazir hook'lar mevcuttur:
 
``````typescript
// Sayfalı liste:
const { data, isLoading, isError } = use${firstPE}(page, size);
const items = data?.data?.items ?? [];
const totalPages = data?.data?.totalPages ?? 1;
const hasNext = data?.data?.hasNextPage ?? false;
 
// Ekle (mutasyon):
const { mutate: add${firstE} } = useAdd${firstE}();
add${firstE}({ name: 'Yeni', price: '99.99' });
 
// Sil:
const { mutate: delete${firstE} } = useDelete${firstE}();
delete${firstE}(itemId);
// Cache otomatik guncellenir (invalidateQueries)
``````
 
---
 
## API Servisi
 
``````typescript
// services/${firstLowE}Service.ts
${firstLowE}Service.getAll() // Tum liste
${firstLowE}Service.getPaged(page, size) // Sayfalı liste
${firstLowE}Service.add(name, price) // Ekle
${firstLowE}Service.remove(id) // Sil (soft delete)
``````
 
API URL: $apiBaseNote (ortam degiskeninden gelir)
 
---
 
## Router
 
$routerNote
 
$(if (-not $isNextJs) { @"
``````
/ -> /{firstLowE}s (redirect)
/login -> LoginPage
/{entity}s -> {Entity}PageV2 (PrivateRoute ile korunmus)
``````
"@ })
 
---
 
## Yeni Entity Sayfasi Ekleme
 
1. ``services/{entity}Service.ts`` ekle (getAll, getPaged, add, remove)
2. ``types/index.ts``'e interface ekle
3. ``hooks/use{Entity}s.ts`` ekle (useQuery + useMutation)
4. ``components/{entity}s/{Entity}Form.tsx`` ekle
5. ``components/{entity}s/{Entity}Table.tsx`` ekle
6. ``pages/{Entity}PageV2.tsx`` ekle
7. ``App.tsx``'de route tanimla
 
---
 
## Ortam Degiskenleri
 
``````
.env <- Gelistirme (http://localhost:8080/api)
.env.docker <- Docker (http://api:8080/api)
``````
 
---
 
## Build
 
``````bash
npm run dev # Gelistirme sunucusu (HMR)
npm run build # Production build (tsc + vite build / next build)
npm run preview # Build onizleme (sadece Vite)
``````
$i18nSection
"@

}

if ($Platform -match "Mobile|Both") {
    $sharedImportNote = if ($Platform -eq "Both") { "import { ApiClient } from 'shared';" } else { "import { ApiClient } from '../shared';" }
    Write-Guide "mobile-guide.md" @"
# Mobil Gelistirici Kilavuzu (React Native / Expo)
 
Uygulama, **Expo Managed Workflow** ve **Expo Router** kullanılarak geliştirilmiştir.
 
---
 
## Klasör Yapısı
 
```
mobile/ (veya apps/mobile/)
+-- app/
| +-- (auth)/login.tsx <- Giriş ekranı
| +-- (tabs)/
| +-- _layout.tsx <- Sekme yönlendirmeleri
| +-- index.tsx <- Ana Dashboard
| +-- {entity}s/ <- Entity listeleme ve ekleme ekranları
| +-- _layout.tsx <- Kök sağlayıcılar (React Query, AuthProvider)
+-- context/
| +-- AuthContext.tsx <- expo-secure-store tabanlı JWT yönetimi
+-- hooks/
| +-- use{Entity}s.ts <- React Query hooks
+-- services/
| +-- api.ts <- Ortak ApiClient örneği
```
 
---
 
## Ortak Paket Kullanımı
 
Web ve Mobil arasındaki kod paylaşımı `shared` paketi üzerinden yönetilir:
 
```typescript
$sharedImportNote
// ApiClient doğrudan enjekte edilen base URL ile çalışır:
export const api = new ApiClient(process.env.EXPO_PUBLIC_API_URL);
```
 
---
 
## Geliştirme ve Çalıştırma
 
Uygulamayı yerel simülatörde veya **Expo Go** uygulaması ile kendi fiziksel cihazınızda çalıştırmak için:
 
```bash
# Bağımlılıkları kurun (kök dizinde)
npm install
 
# Expo Metro bundler sunucusunu başlatın
npm start
```
 
Metro sunucusu başladıktan sonra terminalde çıkan **QR kodu** telefonunuzun kamerası (iOS) veya Expo Go uygulaması (Android) ile taratarak uygulamayı anında cihazınızda test edebilirsiniz.
"@

}