diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e117741..78009c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: jobs: build-and-test: runs-on: ubuntu-latest + + permissions: + contents: read + packages: write steps: - name: đŸ“„ Checkout repository @@ -97,3 +101,63 @@ jobs: with: name: playwright-screenshots path: frontend/screenshots/ + + # ========================================== + # DOCKER PUBLISH TO GHCR (Only on push to main or feature/postman-tests) + # ========================================== + - name: 🔐 Log in to GitHub Container Registry + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🐳 Build and Push Docker Images to GHCR + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') + run: | + OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + + echo "Building and Pushing API Image..." + docker build -t ghcr.io/$OWNER_LC/tradeoffstack-api:latest -f Dockerfile . + docker push ghcr.io/$OWNER_LC/tradeoffstack-api:latest + + echo "Building and Pushing Frontend Image..." + docker build -t ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest -f frontend/Dockerfile frontend/ + docker push ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest + + # ========================================== + # 🚀 CONTINUOUS DEPLOYMENT (CD) TO HOSTINGER VPS + # ========================================== + - name: đŸ“€ Copy Configuration Files to VPS + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.VPS_IP }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "Caddyfile,docker-compose.prod.yml" + target: "/app/tradeoffstack" + + - name: 🚀 Deploy to VPS via SSH + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.VPS_IP }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + cd /app/tradeoffstack + + # Inject production environment variables safely + echo "POSTGRES_USER=tradeoff_admin" > .env + echo "POSTGRES_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env + echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env + + # Pull latest images and update containers + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d --remove-orphans + + # Cleanup unused images to keep disk space free + docker image prune -f + diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..6583f7b --- /dev/null +++ b/Caddyfile @@ -0,0 +1,16 @@ +# Configuration Caddy pour TradeOffStack Production +# Remplacer ':80' par votre domaine (ex: tradeoffstack.com) pour activer le HTTPS Let's Encrypt automatique ! +:80 { + # Rediriger les requĂȘtes API (/api/*) vers le conteneur backend + handle /api/* { + reverse_proxy api:8080 + } + + # Rediriger toutes les autres requĂȘtes vers le conteneur frontend + handle { + reverse_proxy frontend:80 + } + + # Compression des rĂ©ponses pour Ă©conomiser de la bande passante + encode gzip zstd +} diff --git a/Dockerfile b/Dockerfile index 4bbd28c..ca3db25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,12 +25,16 @@ RUN dotnet publish "TradeOffStackAPI.csproj" -c Release -o /app/publish /p:UseAp FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app -# SĂ©curitĂ© : utiliser l'utilisateur non-root 'app' intĂ©grĂ© dans les images .NET 8+ -USER app - # Copier les fichiers compilĂ©s depuis le build stage COPY --from=build /app/publish . +# CrĂ©er le rĂ©pertoire d'uploads et donner les permissions Ă  l'utilisateur app +RUN mkdir -p /app/wwwroot/uploads/Equipments /app/wwwroot/uploads/Users \ + && chown -R app:app /app/wwwroot + +# SĂ©curitĂ© : utiliser l'utilisateur non-root 'app' intĂ©grĂ© dans les images .NET 8+ +USER app + # DĂ©finir le port d'Ă©coute (8080 est le standard par dĂ©faut dans .NET 8+) EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 diff --git a/Technical_Documentation.md b/Technical_Documentation.md new file mode 100644 index 0000000..f3f60e2 --- /dev/null +++ b/Technical_Documentation.md @@ -0,0 +1,38 @@ +# Documentation Technique - TradeOffStack API + +Ce document rĂ©sume l'architecture technique, les choix de sĂ©curitĂ©, et le fonctionnement interne du projet TradeOffStack API. Il sert de rĂ©fĂ©rence pour les dĂ©veloppeurs, DevOps, et auditeurs. + +## 1. Stack Technologique & Architecture +L'API est construite selon les normes modernes de dĂ©veloppement Backend d'Entreprise : +- **Framework** : ASP.NET Core 10.0 (Minimal APIs et Controllers) +- **Base de donnĂ©es** : PostgreSQL 16 +- **ORM** : Entity Framework Core avec migrations automatiques (Code-First) +- **Architecture** : "Repository Pattern" avec `IGenericRepository` pour assurer une sĂ©paration stricte entre la logique mĂ©tier (Services) et l'accĂšs aux donnĂ©es. + +## 2. DevSecOps & SĂ©curitĂ© (Best Practices) +La sĂ©curitĂ© a Ă©tĂ© placĂ©e au cƓur du dĂ©veloppement : +- **Authentification JWT (JSON Web Tokens)** : Les utilisateurs reçoivent un Token sĂ©curisĂ© (exigeant une clĂ© de signature de 256 bits minimum). +- **Gestion des Secrets** : Les mots de passe de production ne sont jamais hardcodĂ©s. L'API utilise un fichier `.env` non versionnĂ© sur Git, et Docker se charge d'injecter la variable `ConnectionStrings__DefaultConnection` de maniĂšre sĂ©curisĂ©e. +- **Hachage des mots de passe** : L'algorithme standard **BCrypt** est utilisĂ© avec salage dynamique pour empĂȘcher les attaques par dictionnaire. +- **Rate Limiting** : Un middleware bloque les requĂȘtes abusives par adresse IP (100 requĂȘtes/minute globales, 10 requĂȘtes/minute sur les routes de Login) pour prĂ©venir les attaques DDoS et le Brute Force. +- **Seeding Automatique** : Sur une base vide, un compte Administrateur par dĂ©faut est gĂ©nĂ©rĂ© dynamiquement Ă  l'initialisation pour prĂ©venir la faille de "l'Ɠuf et la poule" (Chicken & Egg). + +## 3. CI/CD & DĂ©ploiement Continu +Le projet intĂšgre un pipeline GitHub Actions professionnel (`ci.yml`) : +- **Trigger** : ExĂ©cutĂ© Ă  chaque `push` et `pull_request` vers les branches `main` et `develop`. +- **Validation** : Compile le code source en mode "Release" strict. +- **Tests IsolĂ©s** : ExĂ©cute l'intĂ©gralitĂ© de la suite de tests (`TradeOffStackAPI.Tests`) pour garantir la non-rĂ©gression avant tout dĂ©ploiement. + +## 4. Conteneurisation (Docker) +L'API est 100% DockerisĂ©e, prĂȘte pour un hĂ©bergement Cloud / VPS : +- **Multi-Stage Build** : Le `Dockerfile` utilise le SDK lourd pour compiler, puis transfĂšre uniquement l'exĂ©cutable sur une image Runtime Alpine ultra-lĂ©gĂšre. +- **SĂ©curitĂ© Docker** : L'image finale tourne avec l'utilisateur non-root `app` pour empĂȘcher les fuites de privilĂšges kernel. +- **Orchestration locale** : Le fichier `docker-compose.yml` lie automatiquement le conteneur API au conteneur PostgreSQL via un rĂ©seau virtuel interne sĂ©curisĂ©, et vĂ©rifie que la base est prĂȘte (Healthchecks) avant de lancer l'API. + +## 5. QualitĂ© & Tests (QA) +La robustesse du code est assurĂ©e par deux couches de validation : +- **Tests Unitaires & d'IntĂ©gration (xUnit)** : VĂ©rification du comportement des services et du Role-Based Access Control (RBAC). +- **Postman AutomatisĂ©** : Un fichier `TradeOffStackAPI_Tests_Automatises.postman_collection.json` est fourni. Il permet d'exĂ©cuter localement le cycle de vie complet (Authentification, CrĂ©ation, Lecture, Modification, Suppression d'entitĂ©s) et stocke dynamiquement les tokens en mĂ©moire locale. + +--- +*Ce document prouve que l'infrastructure rĂ©pond aux plus hauts standards de rĂ©silience, de maintenabilitĂ© (code en anglais, documentation XML complĂšte) et de sĂ©curitĂ© informatique.* diff --git a/TradeOffStackAPI/Controllers/UploadController.cs b/TradeOffStackAPI/Controllers/UploadController.cs new file mode 100644 index 0000000..af7d161 --- /dev/null +++ b/TradeOffStackAPI/Controllers/UploadController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace TradeOffStackAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class UploadController : ControllerBase +{ + private readonly IWebHostEnvironment _environment; + + public UploadController(IWebHostEnvironment environment) + { + _environment = environment; + } + + [HttpPost] + public async Task UploadImage([FromForm] IFormFile file, [FromForm] string folder = "Equipments") + { + if (file == null || file.Length == 0) + { + return BadRequest(new { message = "No file uploaded." }); + } + + // Validate file extension + var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowedExtensions.Contains(extension)) + { + return BadRequest(new { message = "Invalid file type. Only JPG, JPEG, PNG, GIF, and WEBP are allowed." }); + } + + try + { + // Create uploads directory in wwwroot + var webRootPath = _environment.WebRootPath ?? Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); + var uploadsFolder = Path.Combine(webRootPath, "uploads", folder); + if (!Directory.Exists(uploadsFolder)) + { + Directory.CreateDirectory(uploadsFolder); + } + + // Generate unique filename + var uniqueFileName = $"{Guid.NewGuid()}{extension}"; + var filePath = Path.Combine(uploadsFolder, uniqueFileName); + + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(fileStream); + } + + // Construct URL + var baseUrl = $"{Request.Scheme}://{Request.Host}"; + var fileUrl = $"{baseUrl}/uploads/{folder}/{uniqueFileName}"; + + return Ok(new + { + image_url = fileUrl, + filename = uniqueFileName + }); + } + catch (Exception ex) + { + return StatusCode(500, new { message = $"Internal server error: {ex.Message}" }); + } + } +} diff --git a/TradeOffStackAPI/Controllers/UserController.cs b/TradeOffStackAPI/Controllers/UserController.cs index 65bd911..dbbe082 100644 --- a/TradeOffStackAPI/Controllers/UserController.cs +++ b/TradeOffStackAPI/Controllers/UserController.cs @@ -3,15 +3,18 @@ using TradeOffStackAPI.Auth; using TradeOffStackAPI.Models; using TradeOffStackAPI.Services.Interfaces; +using System.Security.Claims; +using System.Text.Json.Serialization; namespace TradeOffStackAPI.Controllers; /// -/// GĂšre les comptes utilisateurs et leurs permissions (Admin uniquement). +/// +/// GĂšre les comptes utilisateurs et leurs permissions. /// [ApiController] [Route("api/[controller]")] -[Authorize(Roles = Roles.Admin)] // STRICTEMENT RÉSERVÉ À L'ADMIN +[Authorize] // Accessible Ă  tous les utilisateurs authentifiĂ©s public class UserController : ControllerBase { private readonly IUserService _service; @@ -22,6 +25,7 @@ public UserController(IUserService service) } [HttpGet] + [Authorize(Roles = Roles.Admin)] public async Task GetAll() { var response = await _service.GetAllAsync(); @@ -31,11 +35,20 @@ public async Task GetAll() [HttpGet("{id}")] public async Task GetById(Guid id) { + var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + var currentUserRole = User.FindFirstValue(ClaimTypes.Role) ?? User.FindFirstValue("role"); + + if (currentUserRole != Roles.Admin && currentUserId != id.ToString()) + { + return Forbid(); + } + var response = await _service.GetByIdAsync(id); return response.Success ? Ok(response.Data) : NotFound(new { message = response.Message }); } [HttpGet("email/{email}")] + [Authorize(Roles = Roles.Admin)] public async Task GetByEmail(string email) { var response = await _service.GetByEmailAsync(email); @@ -43,6 +56,7 @@ public async Task GetByEmail(string email) } [HttpGet("department/{departmentId}")] + [Authorize(Roles = Roles.Admin)] public async Task GetByDepartment(Guid departmentId) { var response = await _service.GetByDepartmentAsync(departmentId); @@ -50,12 +64,12 @@ public async Task GetByDepartment(Guid departmentId) } [HttpPost] + [Authorize(Roles = Roles.Admin)] public async Task Create([FromBody] User user) { var response = await _service.AddUserAsync(user); if (!response.Success) { - // On pourrait utiliser un switch sur le message pour retourner 409 (Conflict) ou 400 (Bad Request) return Conflict(new { message = response.Message }); } return CreatedAtAction(nameof(GetById), new { id = response.Data!.Id }, response.Data); @@ -66,15 +80,80 @@ public async Task Update(Guid id, [FromBody] User user) { if (id != user.Id) return BadRequest("Object ID does not match route ID."); + + var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + var currentUserRole = User.FindFirstValue(ClaimTypes.Role) ?? User.FindFirstValue("role"); + + if (currentUserRole != Roles.Admin && currentUserId != id.ToString()) + { + return Forbid(); + } + + // Si ce n'est pas un administrateur, on prĂ©serve les champs sensibles pour Ă©viter l'Ă©lĂ©vation de privilĂšges + if (currentUserRole != Roles.Admin) + { + var existingUserResponse = await _service.GetByIdAsync(id); + if (!existingUserResponse.Success || existingUserResponse.Data == null) + return NotFound(new { message = "User not found." }); + + var existingUser = existingUserResponse.Data; + user.Role = existingUser.Role; + user.DepartmentId = existingUser.DepartmentId; + user.IsActive = existingUser.IsActive; + user.Email = existingUser.Email; // EmpĂȘche de changer d'email pour Ă©viter les dĂ©tournements + } var response = await _service.UpdateUserAsync(id, user); return response.Success ? NoContent() : NotFound(new { message = response.Message }); } + [HttpPut("{id}/change-password")] + public async Task ChangePassword(Guid id, [FromBody] ChangePasswordRequest request) + { + var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + var currentUserRole = User.FindFirstValue(ClaimTypes.Role) ?? User.FindFirstValue("role"); + + if (currentUserRole != Roles.Admin && currentUserId != id.ToString()) + { + return Forbid(); + } + + var userResponse = await _service.GetByIdAsync(id); + if (!userResponse.Success || userResponse.Data == null) + return NotFound(new { message = "User not found." }); + + var user = userResponse.Data; + + // Si l'utilisateur n'est pas admin, il doit fournir son mot de passe actuel correct + if (currentUserRole != Roles.Admin) + { + if (string.IsNullOrEmpty(request.OldPassword) || !BCrypt.Net.BCrypt.Verify(request.OldPassword, user.PasswordHash)) + { + return BadRequest(new { message = "Incorrect current password." }); + } + } + + if (string.IsNullOrEmpty(request.NewPassword)) + return BadRequest(new { message = "New password cannot be empty." }); + + var response = await _service.UpdatePasswordAsync(id, request.NewPassword); + return response.Success ? Ok(new { message = "Password updated successfully." }) : BadRequest(new { message = response.Message }); + } + [HttpDelete("{id}")] + [Authorize(Roles = Roles.Admin)] public async Task Delete(Guid id) { var response = await _service.DeleteUserAsync(id); return response.Success ? NoContent() : NotFound(new { message = response.Message }); } +} + +public class ChangePasswordRequest +{ + [JsonPropertyName("old_password")] + public string OldPassword { get; set; } = string.Empty; + + [JsonPropertyName("new_password")] + public string NewPassword { get; set; } = string.Empty; } \ No newline at end of file diff --git a/TradeOffStackAPI/Program.cs b/TradeOffStackAPI/Program.cs index 43833dd..a61443a 100644 --- a/TradeOffStackAPI/Program.cs +++ b/TradeOffStackAPI/Program.cs @@ -40,6 +40,7 @@ app.UseForwardedHeaders(); app.UseExceptionHandling(); app.UseCors("AllowAll"); +app.UseStaticFiles(); if (app.Environment.IsDevelopment()) { diff --git a/TradeOffStackAPI/Services/EquipmentService.cs b/TradeOffStackAPI/Services/EquipmentService.cs index 294cba0..aaef974 100644 --- a/TradeOffStackAPI/Services/EquipmentService.cs +++ b/TradeOffStackAPI/Services/EquipmentService.cs @@ -34,9 +34,17 @@ private void BuildEquipmentImageUrls(Equipment equipment) { if (!string.IsNullOrEmpty(equipment.Image)) { - var url = $"{_r2BaseUrl}/Equipments/{equipment.Image}"; - equipment.ImageUrl = url; - equipment.ImageUrlHttps = url; + if (equipment.Image.StartsWith("http://") || equipment.Image.StartsWith("https://")) + { + equipment.ImageUrl = equipment.Image; + equipment.ImageUrlHttps = equipment.Image; + } + else + { + var url = $"{_r2BaseUrl}/Equipments/{equipment.Image}"; + equipment.ImageUrl = url; + equipment.ImageUrlHttps = url; + } } } diff --git a/TradeOffStackAPI/Services/Interfaces/IUserService.cs b/TradeOffStackAPI/Services/Interfaces/IUserService.cs index e06e158..8a42d69 100644 --- a/TradeOffStackAPI/Services/Interfaces/IUserService.cs +++ b/TradeOffStackAPI/Services/Interfaces/IUserService.cs @@ -12,4 +12,5 @@ public interface IUserService Task> AddUserAsync(User user); Task> UpdateUserAsync(Guid id, User user); Task> DeleteUserAsync(Guid id); + Task> UpdatePasswordAsync(Guid id, string newPassword); } \ No newline at end of file diff --git a/TradeOffStackAPI/Services/UserService.cs b/TradeOffStackAPI/Services/UserService.cs index b95e35a..deffc33 100644 --- a/TradeOffStackAPI/Services/UserService.cs +++ b/TradeOffStackAPI/Services/UserService.cs @@ -21,7 +21,14 @@ private void BuildUserImageUrls(User user) { if (!string.IsNullOrEmpty(user.ProfileImage)) { - user.ProfileImageUrl = $"{_r2BaseUrl}/Users/{user.ProfileImage}"; + if (user.ProfileImage.StartsWith("http://") || user.ProfileImage.StartsWith("https://")) + { + user.ProfileImageUrl = user.ProfileImage; + } + else + { + user.ProfileImageUrl = $"{_r2BaseUrl}/Users/{user.ProfileImage}"; + } } } @@ -135,6 +142,21 @@ public async Task> DeleteUserAsync(Guid id) ? ServiceResponse.Ok(true, "User deleted.") : ServiceResponse.Fail("User not found or failed to delete."); } + + /// + public async Task> UpdatePasswordAsync(Guid id, string newPassword) + { + var existingUser = await _repo.GetByIdAsync(id); + if (existingUser == null) + return ServiceResponse.Fail("User not found."); + + existingUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword); + + var success = await _repo.UpdateAsync(existingUser); + return success + ? ServiceResponse.Ok(true, "Password updated successfully.") + : ServiceResponse.Fail("Failed to update the password."); + } } // Classe pour mapper la configuration de Cloudflare R2 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c00e16a --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + db: + image: postgres:17-alpine + container_name: tradeoffstack-db-prod + environment: + POSTGRES_USER: ${POSTGRES_USER:-tradeoff_admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-Tr@de0ff_Secure!2026_Db#X9} + POSTGRES_DB: tradeoffstack + volumes: + - pg_prod_data:/var/lib/postgresql/data + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tradeoff_admin} -d tradeoffstack"] + interval: 5s + timeout: 5s + retries: 5 + + api: + image: ghcr.io/dordormin/tradeoffstack-api:latest + container_name: tradeoffstack-api-prod + depends_on: + db: + condition: service_healthy + environment: + ConnectionStrings__DefaultConnection: Host=db;Database=tradeoffstack;Username=${POSTGRES_USER:-tradeoff_admin};Password=${POSTGRES_PASSWORD:-Tr@de0ff_Secure!2026_Db#X9} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-Tr@de0ff_Super_Secret_Key_For_JWT_Auth_2026!} + ASPNETCORE_ENVIRONMENT: Production + restart: always + + frontend: + image: ghcr.io/dordormin/tradeoffstack-frontend:latest + container_name: tradeoffstack-frontend-prod + restart: always + + caddy: + image: caddy:2-alpine + container_name: tradeoffstack-proxy-prod + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + depends_on: + - api + - frontend + restart: always + +volumes: + pg_prod_data: + caddy_data: + caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml index 9b0cbfc..6543d52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,8 @@ services: condition: service_healthy ports: - "5000:8080" + volumes: + - uploads:/app/wwwroot/uploads environment: # Injection de la chaĂźne de connexion (Ă©crase le appsettings.json) - ConnectionStrings__DefaultConnection=Host=db;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD};Port=5432; @@ -44,3 +46,5 @@ services: volumes: pgdata: driver: local + uploads: + driver: local diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/code.html new file mode 100644 index 0000000..137af3a --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/code.html @@ -0,0 +1,460 @@ + + + + + +TradeOffStack - Dashboard + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + +
+ +person +
+
+
+ +
+ +
+
+

Dashboard

+

Platform overview and recent operational activity.

+
+
+ +
+
+ +
+ +
+
+Total Assets +devices +
+
+1,204 + +trending_up +2.4% this month + +
+
+ +
+
+Available Gear +check_circle +
+
+86 + + 7.1% of total pool + +
+
+ +
+
+Active Reservations +calendar_clock +
+
+42 + +arrow_right_alt 12 returning today + +
+
+ +
+
+Critical Maintenance +warning +
+
+5 + + Requires immediate action + +
+
+
+ +
+ +
+

Quick Actions

+
+ + +
+
+ +
+
+

Audit Logs

+View All +
+
+ +
+
+person_add +
+
+

+John Doe was assigned MacBook Pro 16" (SN-239) +

+

2 mins ago ‱ System Auto-provision

+
+
+ +
+
+delete_forever +
+
+

+Server R740 was retired by Admin +

+

1 hour ago ‱ EOL Policy Triggered

+
+
+ +
+
+handshake +
+
+

+Sarah Jenkins requested A/V Kit B +

+

3 hours ago ‱ Pending Approval

+
+
+
+
+
+ +
+
+

Upcoming Reservations

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AssetReserved ByStart DateEnd DateStatus
+
+
+laptop_mac +
+
+

Dell XPS 15

+

LPT-0982

+
+
+
+
+
MK
+Mike Kumar +
+
Oct 24, 09:00 AMOct 26, 05:00 PM + + +Confirmed + +
+
+
+videocam +
+
+

Sony A7S III

+

CAM-0014

+
+
+
+
+
AL
+Anna Lee +
+
Oct 25, 10:00 AMOct 25, 02:00 PM + + +Pending + +
+
+
+router +
+
+

Cisco Meraki MX68

+

NET-4491

+
+
+
+
+
DB
+David Bowles +
+
Oct 28, 08:00 AMNov 15, 05:00 PM + + +Confirmed + +
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/screen.png new file mode 100644 index 0000000..19588ee Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/code.html new file mode 100644 index 0000000..3a5fc18 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/code.html @@ -0,0 +1,526 @@ + + + + + +TradeOffStack - Dashboard + + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + +
+ +person +
+
+
+ +
+ +
+
+

Dashboard

+

Platform overview and recent operational activity.

+
+
+ +
+
+ +
+ +
+
+Total Assets +devices +
+
+1,204 + +trending_up +2.4% this month + +
+
+ +
+
+Available Gear +check_circle +
+
+86 + + 7.1% of total pool + +
+
+ +
+
+Active Reservations +calendar_clock +
+
+42 + +arrow_right_alt 12 returning today + +
+
+ +
+
+Critical Maintenance +warning +
+
+5 + + Requires immediate action + +
+
+
+ +
+ +
+

Quick Actions

+
+ + +
+
+ +
+
+

Audit Logs

+View All +
+
+ +
+
+person_add +
+
+

+John Doe was assigned MacBook Pro 16" (SN-239) +

+

2 mins ago ‱ System Auto-provision

+
+
+ +
+
+delete_forever +
+
+

+Server R740 was retired by Admin +

+

1 hour ago ‱ EOL Policy Triggered

+
+
+ +
+
+handshake +
+
+

+Sarah Jenkins requested A/V Kit B +

+

3 hours ago ‱ Pending Approval

+
+
+
+
+
+ +
+
+

Upcoming Reservations

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AssetReserved ByStart DateEnd DateStatus
+
+
+laptop_mac +
+
+

Dell XPS 15

+

LPT-0982

+
+
+
+
+
MK
+Mike Kumar +
+
Oct 24, 09:00 AMOct 26, 05:00 PM + + +Confirmed + +
+
+
+videocam +
+
+

Sony A7S III

+

CAM-0014

+
+
+
+
+
AL
+Anna Lee +
+
Oct 25, 10:00 AMOct 25, 02:00 PM + + +Pending + +
+
+
+router +
+
+

Cisco Meraki MX68

+

NET-4491

+
+
+
+
+
DB
+David Bowles +
+
Oct 28, 08:00 AMNov 15, 05:00 PM + + +Confirmed + +
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/screen.png new file mode 100644 index 0000000..4c55cf2 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v2_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/code.html new file mode 100644 index 0000000..6d0ba02 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/code.html @@ -0,0 +1,549 @@ + + + + + +TradeOffStack - Dashboard + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + +
+AD +
+
+
+ +
+ +
+
+

Dashboard

+

Platform overview and recent operational activity.

+
+
+ +
+
+ +
+ +
+
+Total Assets +
+devices +
+
+
+1,204 + +trending_up +2.4% this month + +
+
+ +
+
+Available Gear +
+check_circle +
+
+
+86 + + 7.1% of total pool + +
+
+ +
+
+Active Reservations +
+calendar_clock +
+
+
+42 + +arrow_right_alt 12 returning today + +
+
+ +
+
+Critical Maintenance +
+warning +
+
+
+5 + + Requires immediate action + +
+
+
+ +
+ +
+

Quick Actions

+
+ + + +
+
+ +
+
+

Audit Logs

+View All chevron_right +
+
+ +
+
+person_add +
+
+

+John Doe was assigned MacBook Pro 16" (SN-239) +

+

+2 mins ago + +System Auto-provision +

+
+
+ +
+
+delete_forever +
+
+

+Server R740 was retired by Admin +

+

+1 hour ago + +EOL Policy Triggered +

+
+
+ +
+
+handshake +
+
+

+Sarah Jenkins requested A/V Kit B +

+

+3 hours ago + +Pending Approval +

+
+
+
+
+
+ +
+
+

Upcoming Reservations

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AssetReserved ByStart DateEnd DateStatus
+
+
+laptop_mac +
+
+

Dell XPS 15

+

LPT-0982

+
+
+
+
+
MK
+Mike Kumar +
+
Oct 24, 09:00 AMOct 26, 05:00 PM + + +Confirmed + +
+
+
+videocam +
+
+

Sony A7S III

+

CAM-0014

+
+
+
+
+
AL
+Anna Lee +
+
Oct 25, 10:00 AMOct 25, 02:00 PM + + +Pending + +
+
+
+router +
+
+

Cisco Meraki MX68

+

NET-4491

+
+
+
+
+
DB
+David Bowles +
+
Oct 28, 08:00 AMNov 15, 05:00 PM + + +Confirmed + +
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/screen.png new file mode 100644 index 0000000..c6f1912 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v3_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/code.html new file mode 100644 index 0000000..b06958d --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/code.html @@ -0,0 +1,397 @@ + + + + + +Dashboard Overview | TradeOffStack + + + + + + + + + + + + + +
+ +
+
+

Dashboard Overview

+

Real-time telemetry and asset utilization across global facilities.

+
+
+ + +
+
+ +
+ +
+
+
+Total Assets +inventory_2 +
+
+14,285 ++12% +
+
+
+
+
+ +
+
+
+Available Gear +check_circle +
+
+8,402 ++5% +
+
+
+
+
+ +
+
+
+Active Reservations +event_available +
+
+1,245 +curr. cycle +
+
+
+
+
+ +
+
+
+Critical Maint. +warning +
+
+42 +-3 +
+
+
+
+
+
+ +
+ +
+
+

Asset Utilization Trend

+
+7D +30D +1Y +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
Peak: 8.2k
+
+
+
+ + + + +
+
+ +
+
+

Recent Audit Logs

+open_in_new +
+
+ +
+
+add_circle +
+
+

New MacBook Pro 16" assigned

+
+To: Sarah Jenkins +2m ago +
+
+
+ +
+
+build +
+
+

Server R4-Rack2 flagged offline

+
+CRITICAL +15m ago +
+
+
+ +
+
+check_circle +
+
+

Software License Renewed

+
+Adobe Creative Cloud +1h ago +
+
+
+ +
+
+swap_horiz +
+
+

Location Transfer Initiated

+
+NY Office → LON Office +3h ago +
+
+
+ +
+
+archive +
+
+

Dell Monitor U2720Q retired

+
+EoL Reached +5h ago +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/screen.png new file mode 100644 index 0000000..9d406ee Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/dashboard_v4_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise/DESIGN.md b/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise/DESIGN.md new file mode 100644 index 0000000..54213df --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise/DESIGN.md @@ -0,0 +1,176 @@ +--- +name: High-Efficiency Enterprise +colors: + surface: '#f8f9ff' + surface-dim: '#cbdbf5' + surface-bright: '#f8f9ff' + surface-container-lowest: '#ffffff' + surface-container-low: '#eff4ff' + surface-container: '#e5eeff' + surface-container-high: '#dce9ff' + surface-container-highest: '#d3e4fe' + on-surface: '#0b1c30' + on-surface-variant: '#5a4138' + inverse-surface: '#213145' + inverse-on-surface: '#eaf1ff' + outline: '#8f7066' + outline-variant: '#e3bfb2' + surface-tint: '#a83900' + primary: '#a43700' + on-primary: '#ffffff' + primary-container: '#cd4700' + on-primary-container: '#fffbff' + inverse-primary: '#ffb59a' + secondary: '#565e74' + on-secondary: '#ffffff' + secondary-container: '#dae2fd' + on-secondary-container: '#5c647a' + tertiary: '#006194' + on-tertiary: '#ffffff' + tertiary-container: '#007bb9' + on-tertiary-container: '#fdfcff' + error: '#ba1a1a' + on-error: '#ffffff' + error-container: '#ffdad6' + on-error-container: '#93000a' + primary-fixed: '#ffdbcf' + primary-fixed-dim: '#ffb59a' + on-primary-fixed: '#380d00' + on-primary-fixed-variant: '#802a00' + secondary-fixed: '#dae2fd' + secondary-fixed-dim: '#bec6e0' + on-secondary-fixed: '#131b2e' + on-secondary-fixed-variant: '#3f465c' + tertiary-fixed: '#cce5ff' + tertiary-fixed-dim: '#93ccff' + on-tertiary-fixed: '#001d31' + on-tertiary-fixed-variant: '#004b73' + background: '#f8f9ff' + on-background: '#0b1c30' + surface-variant: '#d3e4fe' +typography: + display-lg: + fontFamily: Manrope + fontSize: 48px + fontWeight: '800' + lineHeight: 56px + letterSpacing: -0.02em + headline-lg: + fontFamily: Manrope + fontSize: 32px + fontWeight: '700' + lineHeight: 40px + letterSpacing: -0.01em + headline-lg-mobile: + fontFamily: Manrope + fontSize: 24px + fontWeight: '700' + lineHeight: 32px + headline-md: + fontFamily: Manrope + fontSize: 24px + fontWeight: '600' + lineHeight: 32px + title-lg: + fontFamily: Manrope + fontSize: 20px + fontWeight: '600' + lineHeight: 28px + body-lg: + fontFamily: Manrope + fontSize: 18px + fontWeight: '400' + lineHeight: 28px + body-md: + fontFamily: Manrope + fontSize: 16px + fontWeight: '400' + lineHeight: 24px + body-sm: + fontFamily: Manrope + fontSize: 14px + fontWeight: '400' + lineHeight: 20px + label-md: + fontFamily: Manrope + fontSize: 14px + fontWeight: '600' + lineHeight: 20px + letterSpacing: 0.01em + label-sm: + fontFamily: Manrope + fontSize: 12px + fontWeight: '700' + lineHeight: 16px + letterSpacing: 0.03em +rounded: + sm: 0.125rem + DEFAULT: 0.25rem + md: 0.375rem + lg: 0.5rem + xl: 0.75rem + full: 9999px +spacing: + base: 4px + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 40px + container-max: 1280px + gutter: 24px + margin-mobile: 16px + margin-desktop: 32px +--- + +## Brand & Style +The design system focuses on a high-performance B2B SaaS aesthetic tailored for decision-makers and analysts. The brand personality is authoritative yet approachable, emphasizing clarity and precision. By blending a **Corporate Modern** foundation with subtle **Tonal Layering**, the UI evokes a sense of reliability and speed. The visual language prioritizes data density without sacrificing legibility, ensuring that professional users can navigate complex trade-offs with confidence and focus. + +## Colors +This color palette is engineered for WCAG 2.0 AA compliance. The primary brand orange has been intensified to **#E65100** to ensure a 4.5:1 contrast ratio against the slightly darkened background surfaces. + +- **Primary:** A vibrant, high-contrast orange used for primary actions and brand emphasis. +- **Secondary:** A deep Navy used for navigation and high-level headers to provide a grounded, professional structure. +- **Neutral:** A slate-based neutral scale that favors legibility and subtle UI separation. +- **Surfaces:** The background is darkened to **#F1F5F9** (Slate 100) to reduce glare and improve the "pop" of foreground elements. +- **Status Indicators:** Success, Warning, and Error colors are calibrated to their 700-level shades to ensure text-on-color or color-on-white legibility meets the 4.5:1 threshold. + +## Typography +The design system utilizes **Manrope** across all roles to leverage its modern, balanced, and highly legible geometric traits. + +- **Hierarchy:** Use bold and extra-bold weights for display and headlines to create a clear information architecture. +- **Readability:** Body text is set at 16px (md) or 14px (sm) to maintain high data density while remaining accessible. +- **Alignment:** Tighten letter-spacing on larger headings to maintain a professional, "locked-in" appearance. +- **Contrast:** Always use the Secondary Navy (#0F172A) for headings and Neutral Slate (#334155 / Slate 700 or darker) for body text to ensure AA compliance. + +## Layout & Spacing +The layout follows a **Fluid Grid** logic with fixed maximum constraints for desktop readability. + +- **Grid:** A 12-column system is used for desktop (breakpoint 1024px+), shifting to a 4-column system for mobile. +- **Rhythm:** An 8px linear scale (with a 4px half-step for tight components) governs all padding and margins. +- **Consistency:** Use `md` (16px) for internal component padding and `lg` (24px) for spacing between distinct sections or cards. +- **Adaptation:** On mobile, margins reduce to 16px and gutters to 16px to maximize horizontal real estate. + +## Elevation & Depth +Depth is communicated through **Tonal Layers** and precise **Low-Contrast Outlines**. This minimizes visual noise in data-heavy environments. + +- **Level 0 (Background):** #F1F5F9. The base canvas. +- **Level 1 (Cards/Surface):** #FFFFFF. White surfaces sit on the background with a 1px border (#E2E8F0) and a very soft, subtle ambient shadow (4px blur, 2% opacity). +- **Level 2 (Modals/Popovers):** #FFFFFF. These use a more pronounced ambient shadow (12px blur, 8% opacity) to signify interaction priority. +- **Interactive States:** Use a 2px Primary Orange outline for keyboard focus states to ensure maximum accessibility and visibility. + +## Shapes +The design system adopts a **Soft** shape language. + +- **Standard:** 0.25rem (4px) radius for buttons, inputs, and small components. +- **Containers:** 0.5rem (8px) for cards and modals. +- **Selection:** Use the soft radius for checkboxes and radio button containers to maintain a consistent professional look. +- **Buttons:** Avoid pill-shapes; stick to the standard 4px radius to reinforce the structural, SaaS-oriented aesthetic. + +## Components +- **Buttons:** Primary buttons use the Primary Orange (#E65100) with White text. Secondary buttons use a Slate 700 outline with Slate 700 text. +- **Inputs:** Text fields use a 1px #CBD5E1 border, shifting to #E65100 on focus. Labels must be Slate 700 or darker for contrast. +- **Chips:** Status chips use a background tint (10% opacity of the status color) with the full-strength status color for the text (e.g., Success text at #15803D). +- **Cards:** Cards are white with a 1px #E2E8F0 border. Headers within cards should have a subtle bottom border to separate metadata from content. +- **Lists:** Use 16px vertical padding for list items with a 1px separator. Hover states should use #F8FAFC. +- **Progress Indicators:** Use the Tertiary Blue (#0284C7) for neutral progress and Status colors for specific outcomes. \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise_dark/DESIGN.md b/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise_dark/DESIGN.md new file mode 100644 index 0000000..6728238 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/high_efficiency_enterprise_dark/DESIGN.md @@ -0,0 +1,166 @@ +--- +name: High-Efficiency Enterprise Dark +colors: + surface: '#14121a' + surface-dim: '#14121a' + surface-bright: '#3a3841' + surface-container-lowest: '#0f0d15' + surface-container-low: '#1c1a23' + surface-container: '#201e27' + surface-container-high: '#2b2931' + surface-container-highest: '#36333c' + on-surface: '#e6e0ec' + on-surface-variant: '#cac4d6' + inverse-surface: '#e6e0ec' + inverse-on-surface: '#312f38' + outline: '#938e9f' + outline-variant: '#484554' + surface-tint: '#cbbeff' + primary: '#cbbeff' + on-primary: '#340098' + primary-container: '#7a5de6' + on-primary-container: '#fffcff' + inverse-primary: '#6344ce' + secondary: '#5ddac9' + on-secondary: '#003731' + secondary-container: '#00a393' + on-secondary-container: '#00302a' + tertiary: '#ffaedd' + on-tertiary: '#60004a' + tertiary-container: '#bc4898' + on-tertiary-container: '#fffbff' + error: '#ffb4ab' + on-error: '#690005' + error-container: '#93000a' + on-error-container: '#ffdad6' + primary-fixed: '#e7deff' + primary-fixed-dim: '#cbbeff' + on-primary-fixed: '#1e0061' + on-primary-fixed-variant: '#4b26b5' + secondary-fixed: '#7cf7e5' + secondary-fixed-dim: '#5ddac9' + on-secondary-fixed: '#00201c' + on-secondary-fixed-variant: '#005048' + tertiary-fixed: '#ffd8ec' + tertiary-fixed-dim: '#ffaedd' + on-tertiary-fixed: '#3b002d' + on-tertiary-fixed-variant: '#831366' + background: '#14121a' + on-background: '#e6e0ec' + surface-variant: '#36333c' +typography: + headline-lg: + fontFamily: Hanken Grotesk + fontSize: 32px + fontWeight: '600' + lineHeight: '1.2' + letterSpacing: -0.02em + headline-md: + fontFamily: Hanken Grotesk + fontSize: 24px + fontWeight: '600' + lineHeight: '1.3' + headline-sm: + fontFamily: Hanken Grotesk + fontSize: 20px + fontWeight: '500' + lineHeight: '1.4' + body-lg: + fontFamily: Inter + fontSize: 16px + fontWeight: '400' + lineHeight: '1.6' + body-md: + fontFamily: Inter + fontSize: 14px + fontWeight: '400' + lineHeight: '1.5' + label-md: + fontFamily: Geist + fontSize: 12px + fontWeight: '500' + lineHeight: '1' + letterSpacing: 0.05em + mono-sm: + fontFamily: Geist + fontSize: 12px + fontWeight: '400' + lineHeight: '1.5' +rounded: + sm: 0.125rem + DEFAULT: 0.25rem + md: 0.375rem + lg: 0.5rem + xl: 0.75rem + full: 9999px +spacing: + unit: 4px + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 48px + gutter: 16px + margin: 24px +--- + +## Brand & Style +The design system is engineered for high-density enterprise environments where technical precision and rapid data processing are paramount. The brand personality is authoritative, sophisticated, and hyper-functional. + +The design style is a blend of **Minimalism** and **Glassmorphism**, specifically optimized for a professional dark-themed aesthetic. It utilizes deep neutral backgrounds to reduce eye strain, punctuated by vibrant, high-contrast accents that signal state changes and interactive priority. The goal is to evoke an emotional response of absolute control and reliability within a complex software ecosystem. + +## Colors +The palette is anchored in a deep charcoal neutral (`#0f1117`) to establish a professional enterprise foundation. + +- **Primary (#7a5de6):** Used for main action pathways, selection states, and brand presence. +- **Secondary (#1dac9c):** Reserved for data visualization highlights and secondary navigation cues. +- **Tertiary (#f275c7):** An accent color for specialized metadata, badges, and user-specific callouts. +- **Success (#17f748):** Utilized for positive status indicators and completion states. + +To ensure WCAG 2.0 contrast compliance, text on surfaces uses a range of off-whites (90% opacity for headings, 70% for body). Interactive accents are paired with dark backgrounds to maintain a minimum 4.5:1 ratio, while critical alerts use high-luminance variants of the tertiary and success tokens. + +## Typography +This design system utilizes a three-font strategy to balance readability with technical utility: +- **Hanken Grotesk** handles high-level headers, providing a modern, sharp edge to the interface. +- **Inter** is the workhorse for body copy and data tables, chosen for its exceptional legibility in dark mode and high-density layouts. +- **Geist** is reserved for labels, metadata, and code-like values, reinforcing the developer-friendly, technical nature of the platform. + +Mobile typography scales primarily by reducing `headline-lg` to 24px and increasing line-heights slightly to improve touch-target readability. + +## Layout & Spacing +The system employs a **Fluid Grid** model based on a 4px baseline shift. + +- **Desktop:** 12-column grid with 16px gutters. Content is housed in "Containers" that use `16px` padding for internal elements. +- **Tablet:** 8-column grid with 16px gutters. +- **Mobile:** 4-column grid with 12px gutters and 16px side margins. + +Spacing follows a strict geometric progression to ensure visual rhythm. Large sections (e.g., between card groups) use `xl` spacing, while related inputs use `sm` or `xs`. + +## Elevation & Depth +Depth is created through **Tonal Layering** and **Subtle Glassmorphism** rather than traditional heavy shadows. + +- **Level 0 (Base):** Deepest layer (`#0f1117`). +- **Level 1 (Cards/Containers):** Raised surface (`#1a1d26`) with a 1px solid border (`#2d313d`). +- **Level 2 (Modals/Popovers):** Elevated surface with a subtle 10% opacity white border and a large, soft ambient shadow (20px blur, 0.4 opacity). +- **Glass Effect:** Used for fixed navigation bars or headers—applying a `backdrop-filter: blur(12px)` with a 70% transparent surface color to maintain context of the content scrolling beneath. + +## Shapes +The design system adopts a **Soft** shape language. +- Standard components (buttons, inputs) use a `0.25rem` (4px) radius. +- Large containers and cards use `0.5rem` (8px). +- This restrained rounding maintains the professional, "square" enterprise feel while removing the harshness of perfectly sharp corners, ensuring the UI feels modern but disciplined. + +## Components + +### CRUD Actions +Standardized behaviors for core operations to ensure user predictability: +- **Add:** Primary button (`#7a5de6`). Displays a "Plus" icon. Opens a Level 2 Modal or an inline top-row expansion. +- **Edit:** Secondary ghost button with a "Pencil" icon. Changes the surface border of the editable element to Primary (`#7a5de6`) to indicate focus. +- **Delete:** High-contrast outline button. Requires a "Double-tap" or "Hold" interaction for destructive safety, shifting to a solid Tertiary (`#f275c7`) state on the final confirmation. +- **Retire:** A unique state using a "Archive" icon. Visual treatment involves desaturating the element to 50% opacity and applying a "Retired" label using the `label-md` typography. + +### Common UI +- **Buttons:** Solid for Primary; Outlined with `#2d313d` for Secondary. +- **Inputs:** Dark background (`#0f1117`) with a 1px border. On focus, the border transitions to Primary (`#7a5de6`). +- **Chips/Badges:** Small, Geist-font labels with 10% opacity background of the accent color (Primary, Secondary, or Success) and 100% opacity text for contrast compliance. +- **Cards:** Level 1 surfaces. Grouped data should use `md` (16px) internal padding. \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/code.html new file mode 100644 index 0000000..1c50eb0 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/code.html @@ -0,0 +1,457 @@ + + + + + +TradeOffStack - Inventory + + + + + + + + + + + + + +
+ +
+ +
+ + +
+ +
+ + +
+User Avatar +
+
+
+ +
+ +
+
+

Asset Inventory

+

Manage and track IT hardware across the organization.

+
+
+ + +
+
+ +
+
+search + +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSerial NumberCategoryStatusPricePurchase DateActions
+laptop_mac + MacBook Pro 16" + SN-7782910Laptop + + + Assigned + +$2,499.00Oct 12, 2023 + +
+monitor + Dell UltraSharp 27" + DEL-U2723QE-01Monitor + + + Available + +$649.00Nov 05, 2023 + +
+keyboard + Logitech MX Keys + LOG-MXK-092Peripheral + + + In Repair + +$119.00Jan 15, 2023 + +
+smartphone + iPhone 13 Pro + IPH-13P-441Mobile + + + Retired + +$999.00Sep 20, 2021 + +
+
+ +
+Showing 1-4 of 1,204 assets +
+ + +
+
+
+ +
+ +
+
+
+laptop_mac +
+
+

MacBook Pro 16"

+

SN-7782910

+
+
+ +
+ +
+ +
+ + +
+ +
+

Current Assignment

+
+
+Jane Doe +
+
+

Jane Doe

+

Senior Frontend Engineer

+
+ + Assigned + +
+
+ +
+

Specifications

+
+
+

Processor

+

Apple M2 Max

+
+
+

Memory

+

64GB Unified

+
+
+

Storage

+

2TB SSD

+
+
+

Purchase Info

+

$2,499 ‱ Apple Inc.

+
+
+
+ +
+
+

Maintenance Log

+ +
+
+
+
+

Routine Checkup - Passed

+

Dec 01, 2023 ‱ IT Dept

+
+
+
+

Keyboard Replacement

+

Nov 15, 2023 ‱ Sent to Apple Care

+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/screen.png new file mode 100644 index 0000000..b35a56d Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/code.html new file mode 100644 index 0000000..21e5a36 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/code.html @@ -0,0 +1,523 @@ + + + + + +TradeOffStack - Inventory + + + + + + + + + + + + +
+ +
+ +
+ + +
+ +
+ + +
+User Avatar +
+
+
+ +
+ +
+
+

Asset Inventory

+

Manage and track IT hardware across the organization.

+
+
+ + +
+
+ +
+
+search + +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSerial NumberCategoryStatusPricePurchase DateActions
+laptop_mac + MacBook Pro 16" + SN-7782910Laptop + + + Assigned + +$2,499.00Oct 12, 2023 + +
+monitor + Dell UltraSharp 27" + DEL-U2723QE-01Monitor + + + Available + +$649.00Nov 05, 2023 + +
+keyboard + Logitech MX Keys + LOG-MXK-092Peripheral + + + In Repair + +$119.00Jan 15, 2023 + +
+smartphone + iPhone 13 Pro + IPH-13P-441Mobile + + + Retired + +$999.00Sep 20, 2021 + +
+
+ +
+Showing 1-4 of 1,204 assets +
+ + +
+
+
+ +
+ +
+
+
+laptop_mac +
+
+

MacBook Pro 16"

+

SN-7782910

+
+
+ +
+ +
+ +
+ + +
+ +
+

Current Assignment

+
+
+Jane Doe +
+
+

Jane Doe

+

Senior Frontend Engineer

+
+ + Assigned + +
+
+ +
+

Specifications

+
+
+

Processor

+

Apple M2 Max

+
+
+

Memory

+

64GB Unified

+
+
+

Storage

+

2TB SSD

+
+
+

Purchase Info

+

$2,499 ‱ Apple Inc.

+
+
+
+ +
+
+

Maintenance Log

+ +
+
+
+
+

Routine Checkup - Passed

+

Dec 01, 2023 ‱ IT Dept

+
+
+
+

Keyboard Replacement

+

Nov 15, 2023 ‱ Sent to Apple Care

+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/screen.png new file mode 100644 index 0000000..f7ada6b Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v2_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/code.html new file mode 100644 index 0000000..b404974 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/code.html @@ -0,0 +1,533 @@ + + + + + +TradeOffStack - Inventory + + + + + + + + + + + + +
+ +
+ +
+ + +
+ +
+ + +
+User Avatar +
+
+
+ +
+ +
+
+

Asset Inventory

+

Manage and track IT hardware across the organization.

+
+
+ + +
+
+ +
+
+search + +
+
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSerial NumberCategoryLocationStatusPricePurchase DateActions
+laptop_mac + MacBook Pro 16" + SN-7782910LaptopHQ - Floor 3 + + + Assigned + +$2,499.00Oct 12, 2023 + +
+monitor + Dell UltraSharp 27" + DEL-U2723QE-01MonitorNYC - Storage + + + Available + +$649.00Nov 05, 2023 + +
+keyboard + Logitech MX Keys + LOG-MXK-092PeripheralHQ - Floor 2 + + + In Repair + +$119.00Jan 15, 2023 + +
+smartphone + iPhone 13 Pro + IPH-13P-441MobileSF - Offsite + + + Retired + +$999.00Sep 20, 2021 + +
+
+ +
+Showing 1-4 of 1,204 assets +
+ + +
+
+
+ +
+ +
+
+
+laptop_mac +
+
+

MacBook Pro 16"

+

SN-7782910

+
+
+ +
+ +
+ +
+ + + +
+ +
+

Current Assignment

+
+
+Jane Doe +
+
+

Jane Doe

+

Senior Frontend Engineer

+
+ + Assigned + +
+
+ +
+

Specifications

+
+
+

Processor

+

Apple M2 Max

+
+
+

Memory

+

64GB Unified

+
+
+

Storage

+

2TB SSD

+
+
+

Purchase Info

+

$2,499 ‱ Apple Inc.

+
+
+
+ +
+
+

Maintenance Log

+ +
+
+
+
+

Routine Checkup - Passed

+

Dec 01, 2023 ‱ IT Dept

+
+
+
+

Keyboard Replacement

+

Nov 15, 2023 ‱ Sent to Apple Care

+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/screen.png new file mode 100644 index 0000000..3cb8e12 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v3_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/code.html new file mode 100644 index 0000000..4bde807 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/code.html @@ -0,0 +1,490 @@ + + + + + +TradeOffStack - Inventory Management + + + + + + + + + +
+
+TradeOffStack +
+
+ + + +
+
+ + + +
+ +
+
+

Inventory

+

Manage and track all enterprise assets across locations.

+
+
+ + +
+
+ +
+
+search + +
+
+ + + + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Asset NameSerial NumberCategoryStatusLocationActions
+ + +
+
+laptop_mac +
+
+
MacBook Pro 16" M3 Max
+
Assigned: Jane Doe
+
+
+
C02JG8X9Q6L4Laptops + + + In Use + +SF-HQ-Fl3 +
+ + + +
+
+ + +
+
+monitor +
+
+
Dell UltraSharp 32" 4K
+
Unassigned
+
+
+
CN-0W90KW-74261-123-456LMonitors + + + Available + +NY-Stockroom-A +
+ + + +
+
+ + +
+
+keyboard +
+
+
Keychron K2 V2
+
Hardware Failure
+
+
+
K2V2-2021-00192Peripherals + +archive + Retired + +E-Waste Bin B +
+ +
+
+ + +
+
+router +
+
+
Cisco Meraki MR46
+
Assigned: IT Ops
+
+
+
Q2FD-XX99-4A1BNetworking + +warning + Maintenance Due + +LDN-ServerRm-1 +
+ + + +
+
+
+ +
+Showing 1-4 of 1,248 assets +
+ +Page 1 of 312 + +
+
+
+
+ + \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/screen.png new file mode 100644 index 0000000..47bfd97 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/inventory_management_v4_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/code.html new file mode 100644 index 0000000..a3a0300 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/code.html @@ -0,0 +1,350 @@ + + + + + +TradeOffStack - Maintenance + + + + + + + + + + + +
+
+
T
+ TradeOffStack +
+
+notifications +settings +
+
+ +
+ +
+
+

Maintenance Board

+

Track and manage hardware repair cycles.

+
+
+
+ + +
+ +
+
+ +
+ +
+
+
+
+

Triage

+3 +
+ +
+
+ +
+
+TKT-892 +Critical +
+

Server Rack 4 Power Supply Failure

+

Redundant PSU B failed self-test during scheduled diagnostic.

+
+
+
?
+
+ +
+
+ +
+
+TKT-894 +Low +
+

Replace Thermal Paste - Node 12

+

Routine maintenance schedule flag for thermal interface material.

+
+
+
?
+
+ +
+
+
+
+ +
+
+
+
+

In Progress

+2 +
+ +
+
+ +
+
+TKT-889 +High +
+

Switch Port Flapping (Core B)

+

Intermittent connectivity drops on Port 14. Replacing SFP module.

+
+
+Tech Avatar +J. Doe +
+ +
+
+
+
+ +
+
+
+
+

Awaiting Parts

+1 +
+ +
+
+ +
+
+TKT-875 +Medium +
+

Display Artifacts - Workstation 44

+

GPU replacement approved. Waiting on vendor shipment (ETA 2 days).

+
+
+Tech Avatar +
+local_shipping PO-9921 +
+
+
+
+ +
+
+
+
+

Resolved

+24 +
+ +
+
+ +
+
+TKT-880 +check_circle +
+

RAM Upgrade DB-Node 2

+

Upgraded to 256GB ECC.

+
+
+Tech Avatar +
+Closed Today +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/screen.png new file mode 100644 index 0000000..9196249 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/maintenance_management_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/readme.md b/frontend-assets/stitch_tradeoffstack_asset_portal/readme.md new file mode 100644 index 0000000..adff916 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/readme.md @@ -0,0 +1,99 @@ +# 🏱 TradeOffStack API +> **PRIVATE REPOSITORY** – Internal IT Asset Management System + +![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?logo=dotnet) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?logo=postgresql) +![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker) +![CI/CD](https://img.shields.io/badge/CI%2FCD-Active-brightgreen?logo=github-actions) +![Status](https://img.shields.io/badge/Status-Production%20Ready-success) + +## 🎯 Executive Summary +The **TradeOffStack API** is an enterprise-grade backend application designed exclusively for internal IT asset management. It provides a robust, scalable, and highly secure centralized system to track, assign, and maintain the company's hardware fleet (laptops, peripherals, servers, etc.). + +Built with a stateless architecture, it is fully optimized for cloud deployment (VPS/Cloud Native), multi-instance load balancing, and containerized environments. + +--- + +## 🏗 Architecture & Engineering Standards +This API adheres strictly to modern software engineering best practices: +- **N-Tier Architecture**: Clear separation of concerns (Controllers, Services, Repositories). +- **Generic Repository Pattern**: DRY compliance ensuring high maintainability and rapid integration of future domain entities. +- **Stateless & Cloud-Ready**: Session management via JWT, file storage delegated to Cloudflare R2, enabling horizontal scalability. +- **Fail-Fast Initialization**: Safe automated Entity Framework Core database migrations upon startup. +- **Comprehensive Logging & Auditing**: Strict tracking of every action performed on critical assets. + +--- + +## ⚙ Core Modules & Capabilities + +### 🔐 1. Identity & Access Management (IAM) +- **Role-Based Access Control (RBAC)**: Secure access using JWT (JSON Web Tokens) with distinct roles (`Admin`, `IT_Support`, `Employee`). +- **Endpoints**: `POST /api/auth/login`, `POST /api/auth/register`, `GET /api/auth/me`. + +### đŸ’» 2. Asset & Equipment Management +- **Lifecycle Tracking**: Full CRUD operations for IT hardware, including status (`Available`, `InUse`, `InMaintenance`, `Retired`). +- **Endpoints**: `GET /api/equipment`, `POST /api/equipment`, `PUT /api/equipment/{id}`, `GET /api/equipment/category/{category}`. + +### 📅 3. Reservations & Assignments +- **Concurrency Protection**: Strict rules to prevent double-booking or assigning unavailable equipment. +- **Endpoints**: `POST /api/reservation`, `GET /api/reservation/active`, `PUT /api/reservation/{id}/complete`. + +### 🔧 4. Maintenance Requests +- **Ticketing Workflow**: Employees can report issues. IT Support can track repair statuses (`Pending`, `InProgress`, `Resolved`). +- **Endpoints**: `GET /api/maintenance`, `POST /api/maintenance`, `PUT /api/maintenance/{id}/status`. + +### 🏱 5. Department Management +- **Logical Grouping**: Organizes users and tracks assets distributed across different company divisions. +- **Endpoints**: `GET /api/department`, `POST /api/department`. + +### 📜 6. Security Audit Logs +- **Traceability**: Immutable logs of critical system events (e.g., who deleted a server, who updated a user). +- **Endpoints**: `GET /api/auditlog/{entityType}/{entityId}`. + +--- + +## 🚀 Getting Started (Local Development) + +### Prerequisites +- [Docker Desktop](https://www.docker.com/products/docker-desktop) +- [.NET 10 SDK](https://dotnet.microsoft.com/download) + +### Quick Start via Docker Compose +The easiest way to boot the entire stack (PostgreSQL Database + .NET API) is through Docker. + +```bash +# 1. Clone the repository +git clone https://github.com/YourEnterprise/TradeOffStackAPI.git +cd TradeOffStackAPI + +# 2. Boot the infrastructure +docker-compose up -d --build + +# 3. Check logs +docker-compose logs -f api +``` + +The API will be available at: `http://localhost:5000` (or the configured port). + +--- + +## 📚 API Documentation (Swagger) +The API is self-documented using OpenAPI (Swagger). When running in development mode, you can access the visual API explorer and test endpoints directly: + +👉 **Swagger UI**: `http://localhost:5000/swagger/index.html` + +*Note: For secured endpoints, authenticate via `/api/auth/login` and paste the generated JWT in the `Authorize` (Bearer) menu at the top of the Swagger interface.* + +--- + +## đŸ›Ąïž CI/CD Pipeline +Continuous Integration is configured via **GitHub Actions** (`.github/workflows/ci.yml`). +On every `Push` or `Pull Request` to the `main` branch, the pipeline automatically: +1. Restores NuGet dependencies. +2. Compiles the solution (`Release` mode). +3. Executes all Unit & Integration Tests. + +Deployments to pre-production/production are blocked if heuristics or tests fail. + +--- +*Confidentiality Notice: This repository contains proprietary source code belonging to the organization. Unauthorized copying, distribution, or external deployment is strictly prohibited.* diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/code.html new file mode 100644 index 0000000..76323ca --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/code.html @@ -0,0 +1,499 @@ + + + + + +Reservations & Assignments - TradeOffStack + + + + + + + + + +
+
+TradeOffStack +
+
+ + +
+
+ + + +
+ +
+
+

Reservations & Assignments

+

Manage active hardware loans, equipment requests, and deployment schedules.

+
+
+ + +
+
+ +
+
+
+devices +
+Active Loans +
+142 +arrow_upward 12% +
+
+Next 7 days: +24 returning +
+
+
+
+pending_actions +
+Pending Requests +
+38 +Awaiting Approval +
+
+Urgent: +5 requests +
+
+
+
+warning +
+Overdue Returns +
+12 +arrow_upward 3% +
+
+Escalated: +2 assets +
+
+
+ +
+ +
+ +
+
+search + +
+
+ + +
+
+ +
+ +
+
Asset
+
Assigned To
+ +
Status
+ +
+ +
+ +
+
+MacBook Pro 16" M2 +TAG-8921-MBP +
+
+
SK
+Sarah Jenkins +
+ +
+Active Loan +
+ +
+ +
+
+Dell UltraSharp 32" +TAG-4432-MON +
+
+
MJ
+Marcus Johnson +
+ +
+Pending Setup +
+ +
+ +
+
+iPad Pro 12.9" +TAG-9910-IPD +
+
+
EL
+Emily Chen +
+ +
+Overdue +
+ +
+ +
+
+ThinkPad T14 Gen 3 +TAG-1123-THK +
+
+
DR
+David Rodriguez +
+ +
+Active Loan +
+ +
+
+ +
+Showing 1-4 of 180 +
+ + +
+
+
+
+ +
+
+ +
+ +
+
+Active Loan +RES-8892-A +
+

MacBook Pro 16" M2

+

+barcode TAG-8921-MBP +

+
+ +
+ +
+

Assigned To

+
+
SK
+
+Sarah Jenkins +Engineering Dept +
+ +
+
+ +
+

Loan Timeline

+
+
+
+
+
+Checked Out +Admin User +
+Oct 12, 2023 +
+
+
+
+
+
+Due Return +14 days remaining +
+Dec 01, 2023 +
+
+
+
+ +
+
+check_circle +Condition: Excellent +
+

Deployed with standard dev environment pre-installed. Charger and USB-C cable included in kit.

+
+
+ +
+ +
+ + +
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/screen.png new file mode 100644 index 0000000..c0f4b21 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/reservations_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/code.html new file mode 100644 index 0000000..e4a08a2 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/code.html @@ -0,0 +1,368 @@ + + + + + +Security Audit Logs - TradeOffStack + + + + + + + + + + + +
+ + + +
+ +
+
+

Security Audit Logs

+

Comprehensive trail of system events and administrative actions.

+
+
+ +
+
+ +
+
+ +
+search + +
+
+
+ +
+calendar_today + +expand_more +
+
+
+ +
+filter_list + +expand_more +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimestampUserActionEntityIP Address
+
+Oct 24, 2023 +14:32:01 UTC +
+
+
+
JD
+j.doe@tradeoff.com +
+
+
+warning +Failed Login Attempt +
+
-192.168.1.105
+
+Oct 24, 2023 +11:15:44 UTC +
+
+
+
AS
+a.smith@tradeoff.com +
+
+
+add_box +Asset Created +
+
SN-MAC-992110.0.0.42
+
+Oct 24, 2023 +09:05:12 UTC +
+
+
+
AD
+admin@tradeoff.com +
+
+
+admin_panel_settings +User Role Updated +
+
m.jones@tradeoff.com (To: Admin)203.0.113.55
+
+Oct 23, 2023 +16:45:00 UTC +
+
+
+
SYS
+System Auto-Task +
+
+
+event_busy +Reservation Expired/Cancelled +
+
RES-40992Internal
+
+ +
+Showing 1 to 4 of 1,240 entries +
+ + + + +... + +
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/screen.png new file mode 100644 index 0000000..f6d97e7 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/code.html new file mode 100644 index 0000000..7be179e --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/code.html @@ -0,0 +1,486 @@ + + + + + +Security Audit Logs - TradeOffStack + + + + + + + + + + + +
+ + + +
+ +
+
+

Security Audit Logs

+

Comprehensive trail of system events and administrative actions.

+
+
+ + +
+
+ +
+
+ +
+search + +
+
+
+ +
+calendar_today + +expand_more +
+
+
+ +
+filter_list + +expand_more +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimestampUser / ActorAction / DetailsEntity AffectedIP AddressActions
+
+Oct 24, 2023 +14:32:01 UTC +
+
+
+
JD
+
+j.doe@tradeoff.com +User +
+
+
+
+
+warning +Failed Login Attempt +
+Invalid password provided for account. 3rd attempt. +
+
-192.168.1.105 + + +
+
+Oct 24, 2023 +11:15:44 UTC +
+
+
+
AS
+
+a.smith@tradeoff.com +IT Staff +
+
+
+
+
+add_box +Asset Created +
+MacBook Pro 16" added to inventory. +
+
SN-MAC-992110.0.0.42 + +
+
+Oct 24, 2023 +09:05:12 UTC +
+
+
+
AD
+
+admin@tradeoff.com +Super Admin +
+
+
+
+
+admin_panel_settings +User Role Updated +
+Changed role from 'User' to 'Admin'. +
+
m.jones@tradeoff.com203.0.113.55 + + +
+
+Oct 23, 2023 +16:45:00 UTC +
+
+
+
SYS
+
+System Auto-Task +Automated +
+
+
+
+
+event_busy +Reservation Expired +
+Auto-cancelled due to non-pickup within 48h. +
+
RES-40992Internal + +
+
+ +
+Showing 1 to 4 of 1,240 entries +
+ + + + +... + +
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/screen.png new file mode 100644 index 0000000..cd8524c Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v2_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/code.html new file mode 100644 index 0000000..ebca5e1 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/code.html @@ -0,0 +1,425 @@ + + + + + +Security Audit Logs - TradeOffStack + + + + + + + + +
+
+ + + + +
+
+
+
+TradeOffStack +
+ + +
+
+ + +User profile avatar +
+
+ +
+ +
+
+

Security Audit Logs

+

Chronological record of system-wide events and administrative actions.

+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +expand_more +
+
+
+ +
+ +expand_more +
+
+
+ +
+calendar_today + +
+
+
+
+ +
+
+
+

warning Critical Events (24h)

+
+12 +arrow_upward +3% +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TIMESTAMPEVENT / CATEGORYACTORACTION
2023-10-27 14:32:01 UTC +
+Failed Login Attempt +
+gpp_bad +Authentication +
+
+
System (Automated) + +
2023-10-27 14:15:22 UTC +
+Asset Record Updated +
+update +Data Modification +
+
+
+
+User +J. Doe +
+
+ +
2023-10-27 13:45:00 UTC +
+Firewall Rule Modified +
+settings_system_daydream +System Config +
+
+
Admin User + +
2023-10-27 09:00:12 UTC +
+User Session Started +
+login +Authentication +
+
+
+
+Admin +Admin User +
+
+ +
+
+ +
+Showing 1-4 of 1,284 events +
+ +1 / 321 + +
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/screen.png new file mode 100644 index 0000000..b95687d Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/security_audit_logs_v3_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/code.html new file mode 100644 index 0000000..d58f7d9 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/code.html @@ -0,0 +1,461 @@ + + + + + +Self-Service Portal - TradeOffStack + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+User Avatar +
+
+
+ +
+
+ +
+

Hello, Alex. How can we help you today?

+

Manage your assigned equipment, request new assets, or track maintenance tickets from this central portal.

+
+ +
+ +
+ +
+
+

+devices + My Equipment +

+ +
+
+ +
+
+laptop_mac +
+
+
+ + Active + +
+ +
+
+

MacBook Pro 16"

+

TAG: MAC-2023-0042

+
+Assigned: Jan 12, 2023 + +
+
+
+ +
+
+monitor +
+
+
+ + Active + +
+ +
+
+

Dell UltraSharp 27"

+

TAG: MON-2022-1108

+
+Assigned: Nov 05, 2022 + +
+
+
+
+
+ +
+ +
+

+add_shopping_cart + Request New Asset +

+ +
+
+
1
+Category +
+
+
+
2
+Select +
+
+
+
3
+Details +
+
+ +
+ + + +
+
+ +
+
+
+ +
+
+
+

+build_circle + My Tickets +

+ +
+
+ +
+
+TKT-892 +In Progress +
+
Keyboard double-typing issue
+
+ + Medium + +Updated 2h ago +
+
+ +
+
+TKT-904 +Pending +
+
Request external webcam for meetings
+
+ + Low + +Created 1d ago +
+
+ +
+
+TKT-845 +Resolved +
+
VPN Access configuration
+
+ + High + +Closed Oct 12 +
+
+
+
+ +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/screen.png new file mode 100644 index 0000000..841c630 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/code.html new file mode 100644 index 0000000..44df073 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/code.html @@ -0,0 +1,471 @@ + + + + + +Self-Service Portal - TradeOffStack + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+User Avatar +
+
+
+ +
+
+ +
+

Hello, Alex. How can we help you today?

+

Manage your assigned equipment, request new assets, or track maintenance tickets from this central portal.

+
+ +
+ +
+ +
+
+

+devices + My Equipment +

+ +
+
+ +
+
+laptop_mac +
+
+
+ + Active + +
+ +
+
+

MacBook Pro 16"

+

TAG: MAC-2023-0042

+
+Assigned: Jan 12, 2023 + +
+
+
+ +
+
+monitor +
+
+
+ + Active + +
+ +
+
+

Dell UltraSharp 27"

+

TAG: MON-2022-1108

+
+Assigned: Nov 05, 2022 + +
+
+
+
+
+ +
+ +
+

+add_shopping_cart + Request New Asset +

+ +
+
+
1
+Category +
+
+
+
2
+Select +
+
+
+
3
+Details +
+
+ +
+ + + +
+
+ +
+
+
+ +
+
+
+

+build_circle + My Tickets +

+ +
+
+ +
+
+TKT-892 +In Progress +
+
Keyboard double-typing issue
+
+ + Medium + +Updated 2h ago +
+
+ +
+
+TKT-904 +Pending +
+
Request external webcam for meetings
+
+ + Low + +Created 1d ago +
+
+ +
+
+TKT-845 +Resolved +
+
VPN Access configuration
+
+ + High + +Closed Oct 12 +
+
+
+
+ +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/screen.png new file mode 100644 index 0000000..cf3c545 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v2_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/code.html new file mode 100644 index 0000000..1949bca --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/code.html @@ -0,0 +1,497 @@ + + + + + +Self-Service Portal - TradeOffStack + + + + + + + + + + +
+ +
+
+ +
+Home +chevron_right +Dashboard +
+
+
+ + + +
+
+ +
+
+ +
+

Hello, Alex. How can we help you today?

+

Manage your assigned equipment, request new assets, or track maintenance tickets from this central portal.

+
+ +
+ +
+ +
+
+

+devices + My Equipment +

+ +
+
+ +
+
+laptop_mac +
+
+
+ + Active + +
+ +
+
+

MacBook Pro 16"

+

TAG: MAC-2023-0042

+

Apple M2 Max, 32GB RAM, 1TB SSD. Standard engineering build.

+
+Assigned: Jan 12, 2023 + +
+
+
+ +
+
+monitor +
+
+
+ + Active + +
+ +
+
+

Dell UltraSharp 27"

+

TAG: MON-2022-1108

+

4K USB-C Hub Monitor. Located at Desk 4B.

+
+Assigned: Nov 05, 2022 + +
+
+
+
+
+ +
+ +
+

+add_shopping_cart + Request New Asset +

+ +
+
+
1
+Category +
+
+
+
2
+Select +
+
+
+
3
+Details +
+
+ +
+ + + +
+
+ + +
+
+
+ +
+
+
+

+build_circle + My Tickets +

+ +
+
+ +
+ +
+TKT-892 +In Progress +
+
Keyboard double-typing issue
+

The built-in keyboard on my MacBook Pro is double typing 'e' and 't' intermittently.

+
+ + Medium + +Updated 2h ago +
+
+ +
+ +
+TKT-904 +Pending +
+
Request external webcam for meetings
+

Need a 1080p webcam for client presentations as my laptop is usually docked.

+
+ + Low + +Created 1d ago +
+
+ +
+
+TKT-845 +Resolved +
+
VPN Access configuration
+

Unable to connect to the production network VPN from home.

+
+ + High + +Closed Oct 12 +
+
+
+
+ + +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/screen.png new file mode 100644 index 0000000..253e7e9 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/self_service_portal_v3_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/technical_documentation.md b/frontend-assets/stitch_tradeoffstack_asset_portal/technical_documentation.md new file mode 100644 index 0000000..f3f60e2 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/technical_documentation.md @@ -0,0 +1,38 @@ +# Documentation Technique - TradeOffStack API + +Ce document rĂ©sume l'architecture technique, les choix de sĂ©curitĂ©, et le fonctionnement interne du projet TradeOffStack API. Il sert de rĂ©fĂ©rence pour les dĂ©veloppeurs, DevOps, et auditeurs. + +## 1. Stack Technologique & Architecture +L'API est construite selon les normes modernes de dĂ©veloppement Backend d'Entreprise : +- **Framework** : ASP.NET Core 10.0 (Minimal APIs et Controllers) +- **Base de donnĂ©es** : PostgreSQL 16 +- **ORM** : Entity Framework Core avec migrations automatiques (Code-First) +- **Architecture** : "Repository Pattern" avec `IGenericRepository` pour assurer une sĂ©paration stricte entre la logique mĂ©tier (Services) et l'accĂšs aux donnĂ©es. + +## 2. DevSecOps & SĂ©curitĂ© (Best Practices) +La sĂ©curitĂ© a Ă©tĂ© placĂ©e au cƓur du dĂ©veloppement : +- **Authentification JWT (JSON Web Tokens)** : Les utilisateurs reçoivent un Token sĂ©curisĂ© (exigeant une clĂ© de signature de 256 bits minimum). +- **Gestion des Secrets** : Les mots de passe de production ne sont jamais hardcodĂ©s. L'API utilise un fichier `.env` non versionnĂ© sur Git, et Docker se charge d'injecter la variable `ConnectionStrings__DefaultConnection` de maniĂšre sĂ©curisĂ©e. +- **Hachage des mots de passe** : L'algorithme standard **BCrypt** est utilisĂ© avec salage dynamique pour empĂȘcher les attaques par dictionnaire. +- **Rate Limiting** : Un middleware bloque les requĂȘtes abusives par adresse IP (100 requĂȘtes/minute globales, 10 requĂȘtes/minute sur les routes de Login) pour prĂ©venir les attaques DDoS et le Brute Force. +- **Seeding Automatique** : Sur une base vide, un compte Administrateur par dĂ©faut est gĂ©nĂ©rĂ© dynamiquement Ă  l'initialisation pour prĂ©venir la faille de "l'Ɠuf et la poule" (Chicken & Egg). + +## 3. CI/CD & DĂ©ploiement Continu +Le projet intĂšgre un pipeline GitHub Actions professionnel (`ci.yml`) : +- **Trigger** : ExĂ©cutĂ© Ă  chaque `push` et `pull_request` vers les branches `main` et `develop`. +- **Validation** : Compile le code source en mode "Release" strict. +- **Tests IsolĂ©s** : ExĂ©cute l'intĂ©gralitĂ© de la suite de tests (`TradeOffStackAPI.Tests`) pour garantir la non-rĂ©gression avant tout dĂ©ploiement. + +## 4. Conteneurisation (Docker) +L'API est 100% DockerisĂ©e, prĂȘte pour un hĂ©bergement Cloud / VPS : +- **Multi-Stage Build** : Le `Dockerfile` utilise le SDK lourd pour compiler, puis transfĂšre uniquement l'exĂ©cutable sur une image Runtime Alpine ultra-lĂ©gĂšre. +- **SĂ©curitĂ© Docker** : L'image finale tourne avec l'utilisateur non-root `app` pour empĂȘcher les fuites de privilĂšges kernel. +- **Orchestration locale** : Le fichier `docker-compose.yml` lie automatiquement le conteneur API au conteneur PostgreSQL via un rĂ©seau virtuel interne sĂ©curisĂ©, et vĂ©rifie que la base est prĂȘte (Healthchecks) avant de lancer l'API. + +## 5. QualitĂ© & Tests (QA) +La robustesse du code est assurĂ©e par deux couches de validation : +- **Tests Unitaires & d'IntĂ©gration (xUnit)** : VĂ©rification du comportement des services et du Role-Based Access Control (RBAC). +- **Postman AutomatisĂ©** : Un fichier `TradeOffStackAPI_Tests_Automatises.postman_collection.json` est fourni. Il permet d'exĂ©cuter localement le cycle de vie complet (Authentification, CrĂ©ation, Lecture, Modification, Suppression d'entitĂ©s) et stocke dynamiquement les tokens en mĂ©moire locale. + +--- +*Ce document prouve que l'infrastructure rĂ©pond aux plus hauts standards de rĂ©silience, de maintenabilitĂ© (code en anglais, documentation XML complĂšte) et de sĂ©curitĂ© informatique.* diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/tradeoffstack_design_system/DESIGN.md b/frontend-assets/stitch_tradeoffstack_asset_portal/tradeoffstack_design_system/DESIGN.md new file mode 100644 index 0000000..123252c --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/tradeoffstack_design_system/DESIGN.md @@ -0,0 +1,187 @@ +--- +name: TradeOffStack Design System +colors: + surface: '#1a120d' + surface-dim: '#1a120d' + surface-bright: '#423731' + surface-container-lowest: '#140c08' + surface-container-low: '#231a15' + surface-container: '#271e19' + surface-container-high: '#322823' + surface-container-highest: '#3d332d' + on-surface: '#f1dfd7' + on-surface-variant: '#dbc1b5' + inverse-surface: '#f1dfd7' + inverse-on-surface: '#392e29' + outline: '#a38c80' + outline-variant: '#554339' + surface-tint: '#ffb68e' + primary: '#ffb68e' + on-primary: '#542200' + primary-container: '#d9773a' + on-primary-container: '#491c00' + inverse-primary: '#99460a' + secondary: '#bfc7d8' + on-secondary: '#29313e' + secondary-container: '#3f4755' + on-secondary-container: '#adb5c6' + tertiary: '#f6adfc' + on-tertiary: '#50155a' + tertiary-container: '#bc78c3' + on-tertiary-container: '#480c53' + error: '#ffb4ab' + on-error: '#690005' + error-container: '#93000a' + on-error-container: '#ffdad6' + primary-fixed: '#ffdbca' + primary-fixed-dim: '#ffb68e' + on-primary-fixed: '#331200' + on-primary-fixed-variant: '#773300' + secondary-fixed: '#dbe3f4' + secondary-fixed-dim: '#bfc7d8' + on-secondary-fixed: '#141c28' + on-secondary-fixed-variant: '#3f4755' + tertiary-fixed: '#ffd6fe' + tertiary-fixed-dim: '#f6adfc' + on-tertiary-fixed: '#35003f' + on-tertiary-fixed-variant: '#6a2e73' + background: '#1a120d' + on-background: '#f1dfd7' + surface-variant: '#3d332d' + status-available: '#10B981' + status-reserved: '#3B82F6' + status-repair: '#F59E0B' + status-critical: '#EF4444' + surface-charcoal: '#0F1115' + surface-slate: '#1E293B' + border-subtle: '#334155' +typography: + headline-xl: + fontFamily: Manrope + fontSize: 36px + fontWeight: '700' + lineHeight: 44px + letterSpacing: -0.02em + headline-lg: + fontFamily: Manrope + fontSize: 24px + fontWeight: '600' + lineHeight: 32px + letterSpacing: -0.01em + headline-md: + fontFamily: Manrope + fontSize: 20px + fontWeight: '600' + lineHeight: 28px + body-lg: + fontFamily: Manrope + fontSize: 16px + fontWeight: '400' + lineHeight: 24px + body-md: + fontFamily: Manrope + fontSize: 14px + fontWeight: '400' + lineHeight: 20px + body-sm: + fontFamily: Manrope + fontSize: 13px + fontWeight: '400' + lineHeight: 18px + label-md: + fontFamily: Manrope + fontSize: 12px + fontWeight: '600' + lineHeight: 16px + letterSpacing: 0.02em + mono-sm: + fontFamily: Courier Prime + fontSize: 12px + fontWeight: '400' + lineHeight: 16px +rounded: + sm: 0.125rem + DEFAULT: 0.25rem + md: 0.375rem + lg: 0.5rem + xl: 0.75rem + full: 9999px +spacing: + sidebar-width: 260px + header-height: 64px + gutter: 1rem + container-max: 1440px + stack-sm: 0.5rem + stack-md: 1rem + stack-lg: 1.5rem +--- + +## Brand & Style +The design system for this internal IT Asset Management platform is built on the **Corporate / Modern** aesthetic, prioritizing data density and functional clarity. It targets a professional audience of IT Administrators and internal employees, evoking a sense of high-performance reliability and enterprise-grade security. + +Drawing inspiration from high-fidelity B2B SaaS platforms like Linear and Stripe, the visual language utilizes: +- **Functional Density:** Maximizing screen real estate for asset tracking without sacrificing legibility. +- **Precision Detailing:** Crisp 1px borders, subtle monochromatic shifts, and refined typography to indicate hierarchy. +- **Sophisticated Dark/Light Transitions:** Deep charcoals and slate grays for the dark mode provide a developer-friendly environment, while the light mode uses "Paper" white surfaces with cool-gray accents to maintain focus. +- **Action-Oriented Accents:** The "Sahara" orange is used surgically for primary actions and brand presence, ensuring it remains a premium highlight rather than an overwhelming theme. + +## Colors +This design system uses a primary **Dark Mode** by default to cater to IT environments, though it supports a clean light mode implementation. + +### Palette Logic +- **Primary (Sahara #C2652A):** Reserved strictly for primary call-to-actions (CTAs), active navigation states, and brand-identifying icons. +- **Neutrals:** A range of deep charcoals (#0F1115) for backgrounds and slate grays for containers. This provides the "SaaS" depth seen in modern engineering tools. +- **Semantic Status (Critical):** + - **Available (Emerald):** Positive status, equipment ready for deployment. + - **Reserved (Blue):** In-process or staged equipment. + - **OutForRepair (Amber):** Warning state, requires attention but not immediate panic. + - **Retired/Critical (Rose):** Terminal state or urgent system failure. +- **Borders:** Instead of shadows, use 1px solid borders in `#334155` (dark) or `#E2E8F0` (light) to define component boundaries and maintain a "Technical" feel. + +## Typography +**Manrope** is the workhorse of the design system. It provides a contemporary, geometric feel that bridges the gap between approachable and technical. + +- **Data Density:** `body-sm` (13px) is the standard for data tables and list items to maximize information visibility without compromising legibility. +- **Code/IDs:** Use `mono-sm` (Courier Prime) for Asset Tags, Serial Numbers, and JWT Token snippets to distinguish technical strings from human-readable text. +- **Hierarchy:** Headlines use tighter letter-spacing for a "Stripe-like" premium finish. +- **Labels:** Use uppercase for `label-md` when used as table headers or category tags to create a distinct visual rhythm. + +## Layout & Spacing +The layout follows a **Fixed-Fluid Hybrid** model designed for widescreen monitors typical in IT management workflows. + +- **Navigation:** A persistent left-sidebar (260px) houses the primary modules: IAM, Assets, Reservations, Maintenance, Departments, and Audit Logs. +- **Grid:** A 12-column grid is used for dashboard layouts, while asset tables use a fluid-width container with a horizontal scroll overflow for high-column counts. +- **Breakpoints:** + - **Desktop (1280px+):** Sidebar fully expanded. + - **Tablet (768px - 1279px):** Sidebar collapses to icons; 2-column card layouts reflow to 1. + - **Mobile (<767px):** Sidebar moves to a bottom navigation bar or a hamburger overlay; tables transition to "List Cards." +- **Data Density:** Use a 4px baseline grid. Padding within data cells is strictly 8px (sm) or 12px (md) to maintain the high-density requirement. + +## Elevation & Depth +In line with the sophisticated SaaS aesthetic, depth is communicated through **Tonal Layering** and **Low-contrast Outlines** rather than heavy shadows. + +- **The Layering Model:** + - **Level 0 (Background):** Base surface (#0F1115). + - **Level 1 (Cards/Sidebar):** Slightly lighter slate (#1E293B) with a 1px solid border. + - **Level 2 (Modals/Sheets):** Elevated surface with a 12% opacity ambient shadow and a lighter border color to simulate physical protrusion. +- **Glassmorphism:** Reserved exclusively for the global search header and Slide-over sheets to maintain context of the underlying data while performing tasks. +- **Interactions:** Hover states on table rows should use a subtle background tint change (e.g., adding 5% white overlay) rather than an elevation lift. + +## Shapes +The shape language is "Soft" (`0.25rem` or `4px`), reflecting a disciplined and engineered environment. + +- **Input Fields & Buttons:** Use the standard 4px radius. +- **Tags/Chips:** Status indicators for "Available" or "Reserved" use a slightly more rounded 12px radius to differentiate them from interactive buttons. +- **Cards:** Dashboard widgets use a larger `rounded-lg` (8px) to frame content sections softly against the background. +- **Icons:** Use linear, 2px stroke icons with slightly rounded caps to match the Manrope typeface. + +## Components +- **High-Density DataTables:** Feature "Sticky" ID columns and horizontal scrolling. Header rows use `label-md` with a subtle bottom border. Row height should be capped at 40px for "Compact" and 52px for "Standard" view. +- **Slide-over Sheets:** Used for asset details and quick-edits. These slide in from the right, covering 40% of the screen width, utilizing a backdrop blur on the main content area. +- **Kanban Boards:** For Maintenance Requests. Cards are simplified versions of asset records, draggable between "Pending," "InProgress," and "Resolved" columns. +- **Request Wizards:** Multi-step forms for reservations. Use a horizontal stepper at the top with a "Locked" interaction for future steps. +- **Buttons:** + - *Primary:* Sahara background, white text. + - *Secondary:* Ghost style with 1px slate border. + - *Destructive:* Rose/Red text with ghost styling until hover. +- **Status Badges:** Small, dot-indicator paired with text (e.g., a green dot next to "Available"). Use low-saturation background tints with high-saturation text for the badge container. \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/code.html new file mode 100644 index 0000000..23226e4 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/code.html @@ -0,0 +1,542 @@ + + + + + +TradeOffStack - User Management + + + + + + + + + + + + + +
+ +
+
+

User Management

+

Manage platform access, roles, and directory information.

+
+
+ + +
+
+ +
+ +
+search + +
+ +
+
+Filters: + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + User + arrow_downward + + Status + + Actions +
+ + +
+
+ JS +
+
+
John Smith
+
john.smith@example.com
+
+
+
+ + Active + + +
+ + +
+
+ + +
+Avatar +
+
Sarah Connor
+
sarah.c@example.com
+
+
+
+ + Active + + +
+ + +
+
+ + +
+
+ MR +
+
+
Marcus Reed
+
m.reed@example.com
+
+
+
+ + Deactivated + + +
+ + +
+
+ + +
+Avatar +
+
Elena Rostova
+
elena.r@example.com
+
+
+
+ + Active + + +
+ + +
+
+
+ +
+Showing 1 to 4 of 24 entries +
+ +
+ + + +... + +
+ +
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/screen.png new file mode 100644 index 0000000..f16cd2b Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/code.html new file mode 100644 index 0000000..9278b6d --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/code.html @@ -0,0 +1,586 @@ + + + + + +TradeOffStack - User Management + + + + + + + + + + + + +
+
+ +
+
+ +

+ Users + 142 Total +

+

Manage platform access, roles, and directory information.

+
+
+ + +
+
+ +
+ +
+search + +
+ +
+
+ + +
+ +
+ +
+ +
+ + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + User + arrow_downward + + Status + + Actions +
+ + +
+
+ JS +
+
+
John Smith
+
ID: USR-8492
+
+
+
+ + Active + + +
+ + +
+ +
+
+
+ + +
+ +
+
Sarah Connor
+
ID: USR-8493
+
+
+
+ + Active + + +
+ + +
+ +
+
+
+ + +
+
+ MR +
+
+
Marcus Reed
+
ID: USR-8494
+
+
+
+ + Deactivated + + +
+ + +
+
+ + +
+ +
+
Elena Rostova
+
ID: USR-8495
+
+
+
+ + Active + + +
+ + +
+ +
+
+
+
+ +
+Showing 1 to 4 of 142 entries +
+ +
+ + + +... + +
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/screen.png new file mode 100644 index 0000000..c4f3e50 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v2_tradeoffstack/screen.png differ diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/code.html b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/code.html new file mode 100644 index 0000000..0c31ad7 --- /dev/null +++ b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/code.html @@ -0,0 +1,341 @@ + + + + + +User Management - TradeOffStack + + + + + + + + + + + + + +
+ +
+
+

User Management

+

Manage enterprise roles, department access, and account states.

+
+
+
+search + +
+ +
+
+ +
+
+Total Active Users +1,248 +
+
+Admins +12 +
+
+IT Support +45 +
+
+Pending Approvals +8 +
+
+ +
+ +
+
User
+
Role
+
Department
+
Status
+
Actions
+
+ +
+ +
+
+
ER
+
+Elena Rodriguez +elena.r@tradeoffstack.com +
+
+
+Admin +
+
Global IT Operations
+
+
+Active +
+
+ + +
+
+ +
+
+
MJ
+
+Marcus Johnson +mjohnson@tradeoffstack.com +
+
+
+IT Support +
+
Helpdesk Tier 2
+
+
+Active +
+
+ + +
+
+ +
+
+
SL
+
+Sarah Lin +slin@tradeoffstack.com +
+
+
+Employee +
+
Marketing
+
+
+Active +
+
+ + +
+
+ +
+
+
DP
+
+David Patel +dpatel@tradeoffstack.com +
+
+
+Employee +
+
Finance
+
+
+Deactivated +
+
+ +
+
+
+ +
+Showing 1 to 4 of 1,248 entries +
+ + + + +... + +
+
+
+
+ \ No newline at end of file diff --git a/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/screen.png b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/screen.png new file mode 100644 index 0000000..d8e5ad2 Binary files /dev/null and b/frontend-assets/stitch_tradeoffstack_asset_portal/user_management_v3_tradeoffstack/screen.png differ diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cac8c35 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,31 @@ +# ========================================== +# 1. BUILD STAGE +# ========================================== +FROM node:20-alpine AS build +WORKDIR /app + +# Copier les configurations de dépendance +COPY package*.json ./ + +# Installer proprement les dépendances de production +RUN npm ci + +# Copier tout le code source et lancer le build +COPY . . +RUN npm run build + +# ========================================== +# 2. RUNTIME STAGE (Production) +# ========================================== +FROM nginx:alpine + +# Copier le build statique de Vite +COPY --from=build /app/dist /usr/share/nginx/html + +# Appliquer la configuration Nginx personnalisée pour le routage SPA +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Port d'écoute par défaut +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..8d2766e --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8fdaa26..653e525 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { BrowserRouter, Routes, Route, Navigate, Outlet, useNavigate } from 'react-router-dom'; import { AuthProvider, useAuth } from '@/context/AuthContext'; +import { LanguageProvider } from '@/context/LanguageContext'; import { DashboardLayout } from '@/layouts/DashboardLayout'; import { Dashboard } from '@/pages/Dashboard'; import { Inventory } from '@/pages/Inventory'; @@ -10,6 +11,7 @@ import { Maintenance } from '@/pages/Maintenance'; import { Departments } from '@/pages/Departments'; import { Users } from '@/pages/Users'; import { AuditLogs } from '@/pages/AuditLogs'; +import { Settings } from '@/pages/Settings'; import { apiClient } from '@/api/apiClient'; import { Shield, UserPlus, LogIn, Lock, Mail, User, Eye, EyeOff, LayoutGrid } from 'lucide-react'; const ProtectedRoute = ({ children, allowedRoles }: { children?: React.ReactNode, allowedRoles?: string[] }) => { @@ -385,46 +387,62 @@ const LoginForm = () => { }; function App() { + React.useEffect(() => { + const savedTheme = localStorage.getItem('system_theme') || 'dark'; + const root = document.documentElement; + root.className = ''; + if (savedTheme === 'dark') { + root.classList.add('dark'); + } else if (savedTheme === 'cyberpunk') { + root.classList.add('dark', 'theme-cyberpunk'); + } + }, []); + return ( - - - - } /> - - }> - }> - } /> - } /> - - - - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - - - - } /> + + + + + } /> + + }> + }> + } /> + } /> + + + + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + + + + } /> + } /> + } /> + } /> + - - - } /> - - - + + } /> + + + + ); } diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index a295252..9b7cf3a 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,17 +1,20 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import type { UserRole } from '@/types'; +import type { User, UserRole } from '@/types'; +import { apiClient } from '@/api/apiClient'; interface AuthState { isAuthenticated: boolean; role: UserRole | null; token: string | null; userId: string | null; + user: User | null; isLoading: boolean; } interface AuthContextType extends AuthState { - login: (token: string, role: UserRole, userId: string) => void; + login: (token: string, role: UserRole, userId: string) => Promise; logout: () => void; + refreshUser: () => Promise; } const AuthContext = createContext(undefined); @@ -22,20 +25,51 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children role: null, token: null, userId: null, + user: null, isLoading: true, }); - useEffect(() => { - // Check local storage on mount - const token = localStorage.getItem('jwt_token'); - const role = localStorage.getItem('user_role') as UserRole | null; - const userId = localStorage.getItem('user_id'); - - if (token && role && userId) { - setAuthState({ isAuthenticated: true, role, token, userId, isLoading: false }); - } else { - setAuthState({ isAuthenticated: false, role: null, token: null, userId: null, isLoading: false }); + const fetchUserProfile = async (userId: string, token: string) => { + try { + const response = await apiClient.get(`/user/${userId}`, { + headers: { Authorization: `Bearer ${token}` } + }); + return response.data; + } catch (err) { + console.error('Failed to fetch user profile during initialization', err); + return null; } + }; + + useEffect(() => { + const initAuth = async () => { + const token = localStorage.getItem('jwt_token'); + const role = localStorage.getItem('user_role') as UserRole | null; + const userId = localStorage.getItem('user_id'); + + if (token && role && userId) { + const userDetails = await fetchUserProfile(userId, token); + setAuthState({ + isAuthenticated: true, + role, + token, + userId, + user: userDetails, + isLoading: false, + }); + } else { + setAuthState({ + isAuthenticated: false, + role: null, + token: null, + userId: null, + user: null, + isLoading: false, + }); + } + }; + + initAuth(); // Global listener for 401 Unauthorized from Axios const handleUnauthorized = () => { @@ -46,22 +80,53 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children return () => window.removeEventListener('auth:unauthorized', handleUnauthorized); }, []); - const login = (token: string, role: UserRole, userId: string) => { + const login = async (token: string, role: UserRole, userId: string) => { localStorage.setItem('jwt_token', token); localStorage.setItem('user_role', role); localStorage.setItem('user_id', userId); - setAuthState({ isAuthenticated: true, role, token, userId, isLoading: false }); + + const userDetails = await fetchUserProfile(userId, token); + + setAuthState({ + isAuthenticated: true, + role, + token, + userId, + user: userDetails, + isLoading: false, + }); }; const logout = () => { localStorage.removeItem('jwt_token'); localStorage.removeItem('user_role'); localStorage.removeItem('user_id'); - setAuthState({ isAuthenticated: false, role: null, token: null, userId: null, isLoading: false }); + setAuthState({ + isAuthenticated: false, + role: null, + token: null, + userId: null, + user: null, + isLoading: false, + }); + }; + + const refreshUser = async () => { + if (authState.userId && authState.token) { + try { + const response = await apiClient.get(`/user/${authState.userId}`); + setAuthState((prev) => ({ + ...prev, + user: response.data, + })); + } catch (err) { + console.error('Failed to refresh user profile', err); + } + } }; return ( - + {children} ); diff --git a/frontend/src/context/LanguageContext.tsx b/frontend/src/context/LanguageContext.tsx new file mode 100644 index 0000000..1829187 --- /dev/null +++ b/frontend/src/context/LanguageContext.tsx @@ -0,0 +1,181 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +type Language = 'en' | 'fr'; + +export const translations = { + en: { + dashboard: "Dashboard", + inventory: "Inventory", + myGear: "My Gear", + reservations: "Reservations", + maintenance: "Maintenance", + departments: "Departments", + users: "Users", + auditLogs: "Audit Logs", + signOut: "Sign Out", + settings: "Settings", + settingsDesc: "Manage your workspace preferences, personal details, and security.", + profileSettings: "My Profile Settings", + systemSettings: "System Settings", + personalProfile: "Personal Profile", + personalProfileDesc: "Update your personal information and contact details.", + firstName: "First Name", + lastName: "Last Name", + phoneNumber: "Phone Number", + profileImage: "Profile Image URL / Path", + profileImageHint: "Specify an image name or a custom web image URL to customize your avatar.", + saveProfile: "Save Profile Details", + securityPassword: "Security & Password", + securityPasswordDesc: "Update your login credentials to secure your workspace account.", + currentPassword: "Current Password", + newPassword: "New Password", + confirmNewPassword: "Confirm New Password", + updatePassword: "Update Password", + systemPreferences: "System Preferences", + systemPreferencesDesc: "Configure localization settings, interface themes, and visual parameters.", + systemInterfaceTheme: "System Interface Theme", + darkMode: "Dark mode", + darkModeDesc: "Warm obsidian scheme", + lightMode: "Light mode", + lightModeDesc: "Clean & high contrast", + cyberpunkMode: "Cyberpunk", + cyberpunkModeDesc: "Neon cyan & fuchsia", + localizationLanguage: "Localization Language", + localizationLanguageDesc: "Change the primary translation system for labels.", + systemNotifications: "System Notifications", + systemNotificationsDesc: "Receive workspace triggers and reservation warnings on desktop.", + developerLogs: "Developer Logs", + developerLogsDesc: "Print audit trace events and API calls in the browser logs.", + accessRole: "Access Role", + department: "Department", + status: "Status", + activeAccount: "Active Account", + inactiveAccount: "Inactive", + profileSuccess: "Profile details successfully updated!", + passwordSuccess: "Password successfully updated!", + themeSuccess: "System theme updated successfully!", + languageSuccess: "System language preference saved!", + notificationsEnabled: "Desktop notifications enabled", + notificationsDisabled: "Desktop notifications disabled", + devLogsEnabled: "Developer mode log auditing active", + devLogsDisabled: "Developer mode log auditing disabled", + saving: "Saving...", + updating: "Updating...", + none: "None" + }, + fr: { + dashboard: "Tableau de bord", + inventory: "Inventaire", + myGear: "Mon équipement", + reservations: "Réservations", + maintenance: "Maintenance", + departments: "Départements", + users: "Utilisateurs", + auditLogs: "Journaux d'audit", + signOut: "Se déconnecter", + settings: "ParamÚtres", + settingsDesc: "Gérez vos préférences d'espace de travail, vos informations personnelles et votre sécurité.", + profileSettings: "ParamÚtres du profil", + systemSettings: "ParamÚtres systÚme", + personalProfile: "Profil personnel", + personalProfileDesc: "Mettez à jour vos informations personnelles et vos coordonnées.", + firstName: "Prénom", + lastName: "Nom", + phoneNumber: "Numéro de téléphone", + profileImage: "URL / Chemin de l'image de profil", + profileImageHint: "Spécifiez un nom d'image ou une URL d'image personnalisée pour personnaliser votre avatar.", + saveProfile: "Enregistrer le profil", + securityPassword: "Sécurité & Mot de passe", + securityPasswordDesc: "Mettez à jour vos identifiants pour sécuriser votre compte.", + currentPassword: "Mot de passe actuel", + newPassword: "Nouveau mot de passe", + confirmNewPassword: "Confirmer le nouveau mot de passe", + updatePassword: "Modifier le mot de passe", + systemPreferences: "Préférences systÚme", + systemPreferencesDesc: "Configurez les paramÚtres de localisation, les thÚmes de l'interface et les paramÚtres visuels.", + systemInterfaceTheme: "ThÚme de l'interface systÚme", + darkMode: "Mode sombre", + darkModeDesc: "Schéma d'obsidienne chaleureux", + lightMode: "Mode clair", + lightModeDesc: "Propre & contraste élevé", + cyberpunkMode: "Cyberpunk", + cyberpunkModeDesc: "Néon cyan & fuchsia", + localizationLanguage: "Langue de localisation", + localizationLanguageDesc: "Modifiez le systÚme de traduction principal pour les libellés.", + systemNotifications: "Notifications systÚme", + systemNotificationsDesc: "Recevez les alertes et les avertissements de réservation sur le bureau.", + developerLogs: "Journaux développeur", + developerLogsDesc: "Affichez les événements d'audit et les appels d'API dans la console.", + accessRole: "RÎle d'accÚs", + department: "Département", + status: "Statut", + activeAccount: "Compte actif", + inactiveAccount: "Inactif", + profileSuccess: "Détails du profil mis à jour avec succÚs !", + passwordSuccess: "Mot de passe mis à jour avec succÚs !", + themeSuccess: "ThÚme systÚme mis à jour avec succÚs !", + languageSuccess: "Préférence de langue enregistrée !", + notificationsEnabled: "Notifications de bureau activées", + notificationsDisabled: "Notifications de bureau désactivées", + devLogsEnabled: "Audit des journaux en mode développeur actif", + devLogsDisabled: "Audit des journaux en mode développeur désactivé", + saving: "Enregistrement...", + updating: "Mise à jour...", + none: "Aucun" + } +}; + +interface LanguageContextType { + language: Language; + setLanguage: (lang: Language) => void; + t: (key: keyof typeof translations['en']) => string; +} + +const LanguageContext = createContext(undefined); + +export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [language, setLang] = useState(() => { + return (localStorage.getItem('system_lang') as Language) || 'en'; + }); + + const setLanguage = (lang: Language) => { + setLang(lang); + localStorage.setItem('system_lang', lang); + // Dispatch a custom event to notify other components/instances if needed + window.dispatchEvent(new Event('languagechange')); + }; + + useEffect(() => { + const handleStorageChange = () => { + const stored = localStorage.getItem('system_lang') as Language; + if (stored && stored !== language) { + setLang(stored); + } + }; + window.addEventListener('languagechange', handleStorageChange); + window.addEventListener('storage', handleStorageChange); + return () => { + window.removeEventListener('languagechange', handleStorageChange); + window.removeEventListener('storage', handleStorageChange); + }; + }, [language]); + + const t = (key: keyof typeof translations['en']): string => { + const dict = translations[language] || translations['en']; + return dict[key] || translations['en'][key] || String(key); + }; + + return ( + + {children} + + ); +}; + +export const useTranslation = () => { + const context = useContext(LanguageContext); + if (context === undefined) { + throw new Error('useTranslation must be used within a LanguageProvider'); + } + return context; +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index b75bfba..84ed9b6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -206,4 +206,30 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); +} + +.theme-cyberpunk { + --background: oklch(0.12 0.05 320); + --foreground: oklch(0.9 0.1 180); + --card: oklch(0.15 0.06 320); + --card-foreground: oklch(0.95 0.05 180); + --primary: oklch(0.7 0.3 330); + --primary-foreground: oklch(0.1 0.02 330); + --secondary: oklch(0.18 0.08 190); + --secondary-foreground: oklch(0.85 0.15 190); + --muted: oklch(0.18 0.08 320); + --muted-foreground: oklch(0.6 0.1 320); + --accent: oklch(0.7 0.3 330); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.6 0.25 20); + --border: oklch(0.7 0.3 330 / 25%); + --input: oklch(0.7 0.3 330 / 30%); + --ring: oklch(0.9 0.1 180); + --sidebar: oklch(0.12 0.05 320); + --sidebar-foreground: oklch(0.9 0.1 180); + --sidebar-primary: oklch(0.7 0.3 330); + --sidebar-primary-foreground: oklch(0.1 0.02 330); + --sidebar-accent: oklch(0.18 0.08 190); + --sidebar-accent-foreground: oklch(0.85 0.15 190); + --sidebar-border: oklch(0.7 0.3 330 / 20%); } \ No newline at end of file diff --git a/frontend/src/layouts/DashboardLayout.tsx b/frontend/src/layouts/DashboardLayout.tsx index 48d37fd..fb62afa 100644 --- a/frontend/src/layouts/DashboardLayout.tsx +++ b/frontend/src/layouts/DashboardLayout.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { useAuth } from '@/context/AuthContext'; +import { useTranslation } from '@/context/LanguageContext'; import { LayoutDashboard, MonitorSmartphone, @@ -9,13 +10,17 @@ import { Users, Building2, ShieldAlert, - LogOut + LogOut, + Settings, + User } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; export const DashboardLayout: React.FC = () => { - const { role, logout } = useAuth(); + const { role, user, logout } = useAuth(); + const { t } = useTranslation(); const navigate = useNavigate(); + const [dropdownOpen, setDropdownOpen] = useState(false); const handleLogout = () => { logout(); @@ -23,14 +28,14 @@ export const DashboardLayout: React.FC = () => { }; const navItems = [ - { name: 'Dashboard', path: '/dashboard', icon: LayoutDashboard, roles: ['Admin', 'Manager', 'Employee'] }, - { name: 'Inventory', path: '/inventory', icon: MonitorSmartphone, roles: ['Admin', 'Manager'] }, - { name: 'My Gear', path: '/my-gear', icon: MonitorSmartphone, roles: ['Employee'] }, - { name: 'Reservations', path: '/reservations', icon: CalendarClock, roles: ['Admin', 'Manager', 'Employee'] }, - { name: 'Maintenance', path: '/maintenance', icon: Wrench, roles: ['Admin', 'Manager', 'Employee'] }, - { name: 'Departments', path: '/departments', icon: Building2, roles: ['Admin'] }, - { name: 'Users', path: '/users', icon: Users, roles: ['Admin'] }, - { name: 'Audit Logs', path: '/audit-logs', icon: ShieldAlert, roles: ['Admin'] }, + { labelKey: 'dashboard' as const, path: '/dashboard', icon: LayoutDashboard, roles: ['Admin', 'Manager', 'Employee'] }, + { labelKey: 'inventory' as const, path: '/inventory', icon: MonitorSmartphone, roles: ['Admin', 'Manager'] }, + { labelKey: 'myGear' as const, path: '/my-gear', icon: MonitorSmartphone, roles: ['Employee'] }, + { labelKey: 'reservations' as const, path: '/reservations', icon: CalendarClock, roles: ['Admin', 'Manager', 'Employee'] }, + { labelKey: 'maintenance' as const, path: '/maintenance', icon: Wrench, roles: ['Admin', 'Manager', 'Employee'] }, + { labelKey: 'departments' as const, path: '/departments', icon: Building2, roles: ['Admin'] }, + { labelKey: 'users' as const, path: '/users', icon: Users, roles: ['Admin'] }, + { labelKey: 'auditLogs' as const, path: '/audit-logs', icon: ShieldAlert, roles: ['Admin'] }, ]; const filteredNavItems = navItems.filter((item) => role && item.roles.includes(role)); @@ -62,7 +67,7 @@ export const DashboardLayout: React.FC = () => { } > - {item.name} + {t(item.labelKey)} ))} @@ -70,10 +75,10 @@ export const DashboardLayout: React.FC = () => {
@@ -83,18 +88,63 @@ export const DashboardLayout: React.FC = () => { {/* Header */}
- {/* Contextual Title can go here via React Context or Router Matches */} Asset Portal
-
Logged in as
+
+ {user ? `${user.first_name} ${user.last_name || ''}`.trim() || user.email : t('signOut')} +
{role}
- - - U - +
+ setDropdownOpen(!dropdownOpen)}> + + + {user?.first_name ? user.first_name[0].toUpperCase() : 'U'} + + + + {dropdownOpen && ( + <> +
setDropdownOpen(false)} /> +
+
+
+ {user ? `${user.first_name} ${user.last_name || ''}`.trim() : 'User'} +
+
{user?.email}
+
+ + + + + +
+ + +
+ + )} +
diff --git a/frontend/src/pages/AuditLogs.tsx b/frontend/src/pages/AuditLogs.tsx index da60d93..336783b 100644 --- a/frontend/src/pages/AuditLogs.tsx +++ b/frontend/src/pages/AuditLogs.tsx @@ -19,8 +19,11 @@ import { import type { AuditLog, AuditAction } from '@/types'; import { ShieldAlert, Eye } from 'lucide-react'; import { apiClient } from '@/api/apiClient'; +import { useTranslation } from '@/context/LanguageContext'; export const AuditLogs: React.FC = () => { + const { language } = useTranslation(); + const isFr = language === 'fr'; const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedLog, setSelectedLog] = useState(null); @@ -44,18 +47,18 @@ export const AuditLogs: React.FC = () => { const getActionBadge = (action: AuditAction) => { switch (action) { case 'Created': - return Created; + return {isFr ? 'Créé' : 'Created'}; case 'Updated': - return Updated; + return {isFr ? 'Mis à jour' : 'Updated'}; case 'Deleted': - return Deleted; + return {isFr ? 'Supprimé' : 'Deleted'}; default: return {action}; } }; const renderJsonPretty = (jsonStr?: string) => { - if (!jsonStr) return No records; + if (!jsonStr) return {isFr ? 'Aucun enregistrement' : 'No records'}; try { const parsed = JSON.parse(jsonStr); return ( @@ -76,25 +79,31 @@ export const AuditLogs: React.FC = () => {
-

Security Audit Logs

-

Review system changes, asset modifications, and operator history.

+

+ {isFr ? 'Journaux d\'audit de sécurité' : 'Security Audit Logs'} +

+

+ {isFr ? 'Consultez les modifications systÚme, les modifications d\'équipements et l\'historique des opérateurs.' : 'Review system changes, asset modifications, and operator history.'} +

{isLoading ? (
-
Loading audit journal...
+
+ {isFr ? 'Chargement du journal d\'audit...' : 'Loading audit journal...'} +
) : (
- Timestamp - Entity Type + {isFr ? 'Date et heure' : 'Timestamp'} + {isFr ? 'Type d\'entité' : 'Entity Type'} Action - Entity ID - Performed By + {isFr ? 'ID d\'entité' : 'Entity ID'} + {isFr ? 'Effectué par' : 'Performed By'} Details @@ -102,19 +111,19 @@ export const AuditLogs: React.FC = () => { {logs.length === 0 ? ( - No logs found. + {isFr ? 'Aucun journal d\'audit trouvé.' : 'No logs found.'} ) : ( logs.map((log) => { const userName = log.performed_by ? `${log.performed_by.first_name} ${log.performed_by.last_name}` - : 'System Admin'; + : (isFr ? 'Admin systÚme' : 'System Admin'); return ( - {new Date(log.performed_at).toLocaleString()} + {new Date(log.performed_at).toLocaleString(isFr ? 'fr-FR' : 'en-US')} {log.entity_type} {getActionBadge(log.action)} @@ -125,10 +134,10 @@ export const AuditLogs: React.FC = () => { onClick={() => setSelectedLog(log)} size="sm" variant="outline" - className="border-border hover:bg-secondary flex items-center gap-1 ml-auto" + className="border-border hover:bg-secondary flex items-center gap-1 ml-auto cursor-pointer" > - View Data + {isFr ? 'Voir les données' : 'View Data'} @@ -146,10 +155,12 @@ export const AuditLogs: React.FC = () => { - Audit Transaction Data + {isFr ? 'Données de transaction d\'audit' : 'Audit Transaction Data'} - Inspection of state changes recorded on {selectedLog && new Date(selectedLog.performed_at).toLocaleString()}. + {isFr + ? `Inspection des changements d'état enregistrés le ${selectedLog && new Date(selectedLog.performed_at).toLocaleString(isFr ? 'fr-FR' : 'en-US')}.` + : `Inspection of state changes recorded on ${selectedLog && new Date(selectedLog.performed_at).toLocaleString(isFr ? 'fr-FR' : 'en-US')}.`} @@ -157,35 +168,39 @@ export const AuditLogs: React.FC = () => {
- Entity Type + {isFr ? 'Type d\'entité' : 'Entity Type'} {selectedLog.entity_type}
- Entity Unique ID + {isFr ? 'ID unique d\'entité' : 'Entity Unique ID'} {selectedLog.entity_id}
- Action Performed + {isFr ? 'Action effectuée' : 'Action Performed'} {getActionBadge(selectedLog.action)}
- Operator + {isFr ? 'Opérateur' : 'Operator'} {selectedLog.performed_by ? `${selectedLog.performed_by.first_name} ${selectedLog.performed_by.last_name} (${selectedLog.performed_by.email})` - : 'System Admin'} + : (isFr ? 'Admin systÚme' : 'System Admin')}
- + {renderJsonPretty(selectedLog.old_values)}
- + {renderJsonPretty(selectedLog.new_values)}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 530f645..bd23022 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { MonitorSmartphone, CalendarClock, Wrench, ShieldAlert, Plus, AlertCircle } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; +import { useTranslation } from '@/context/LanguageContext'; import { apiClient } from '@/api/apiClient'; import type { Equipment, Reservation, MaintenanceRequest, AuditLog } from '@/types'; import { useNavigate } from 'react-router-dom'; @@ -26,6 +27,8 @@ interface ActivityItem { export const Dashboard: React.FC = () => { const { role } = useAuth(); + const { language } = useTranslation(); + const isFr = language === 'fr'; const navigate = useNavigate(); const [stats, setStats] = useState({ totalAssets: 0, @@ -45,12 +48,12 @@ export const Dashboard: React.FC = () => { const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - return `${diffDays}d ago`; + if (diffMins < 1) return isFr ? 'À l\'instant' : 'Just now'; + if (diffMins < 60) return isFr ? `Il y a ${diffMins}m` : `${diffMins}m ago`; + if (diffHours < 24) return isFr ? `Il y a ${diffHours}h` : `${diffHours}h ago`; + return isFr ? `Il y a ${diffDays}j` : `${diffDays}d ago`; } catch { - return 'Recent'; + return isFr ? 'RĂ©cemment' : 'Recent'; } }; @@ -58,7 +61,6 @@ export const Dashboard: React.FC = () => { const fetchDashboardData = async () => { setIsLoading(true); try { - // Fetch core entities in parallel const [equipRes, reserveRes, maintRes] = await Promise.all([ apiClient.get('/equipment').catch(() => ({ data: [] })), apiClient.get('/reservation').catch(() => ({ data: [] })), @@ -69,7 +71,6 @@ export const Dashboard: React.FC = () => { const reservations = reserveRes.data || []; const maintenances = maintRes.data || []; - // Calculate stats const totalAssets = equipments.length; const availableGear = equipments.filter(e => e.status === 'Available').length; const activeReservations = reservations.filter(r => r.status === 'Active').length; @@ -84,10 +85,8 @@ export const Dashboard: React.FC = () => { criticalMaintenances, }); - // Resolve item names helper maps const equipMap = new Map(equipments.map(e => [e.id, e.name])); - // Load activities if (role === 'Admin') { try { const auditRes = await apiClient.get('/auditlog'); @@ -97,12 +96,24 @@ export const Dashboard: React.FC = () => { let targetLabel = log.entity_id; if (log.entity_type === 'Equipment') { - targetLabel = equipMap.get(log.entity_id) || `Asset ID: ${log.entity_id.slice(0, 8)}`; - actionLabel = log.action === 'Created' ? 'New Asset Added' : log.action === 'Updated' ? 'Asset Updated' : 'Asset Removed'; + targetLabel = equipMap.get(log.entity_id) || `Équipement ID: ${log.entity_id.slice(0, 8)}`; + if (isFr) { + actionLabel = log.action === 'Created' ? 'Nouvel Ă©quipement créé' : log.action === 'Updated' ? 'Équipement mis Ă  jour' : 'Équipement supprimĂ©'; + } else { + actionLabel = log.action === 'Created' ? 'New Asset Added' : log.action === 'Updated' ? 'Asset Updated' : 'Asset Removed'; + } } else if (log.entity_type === 'Reservation') { - actionLabel = log.action === 'Created' ? 'Reservation Request' : log.action === 'Updated' ? 'Reservation Modified' : 'Reservation Cancelled'; + if (isFr) { + actionLabel = log.action === 'Created' ? 'RĂ©servation demandĂ©e' : log.action === 'Updated' ? 'RĂ©servation modifiĂ©e' : 'RĂ©servation annulĂ©e'; + } else { + actionLabel = log.action === 'Created' ? 'Reservation Request' : log.action === 'Updated' ? 'Reservation Modified' : 'Reservation Cancelled'; + } } else if (log.entity_type === 'MaintenanceRequest') { - actionLabel = log.action === 'Created' ? 'Maintenance Opened' : log.action === 'Updated' ? 'Maintenance Updated' : 'Maintenance Request'; + if (isFr) { + actionLabel = log.action === 'Created' ? 'Maintenance ouverte' : log.action === 'Updated' ? 'Maintenance mise Ă  jour' : 'Demande de maintenance'; + } else { + actionLabel = log.action === 'Created' ? 'Maintenance Opened' : log.action === 'Updated' ? 'Maintenance Updated' : 'Maintenance Request'; + } } const userName = log.performed_by @@ -137,13 +148,12 @@ export const Dashboard: React.FC = () => { setActivities([]); } } else { - // Non-admin fallback to list recent reservations and maintenance requests const recentList: ActivityItem[] = []; reservations.slice(0, 3).forEach(r => { recentList.push({ id: r.id, - action: `Equipment Assigned`, + action: isFr ? 'MatĂ©riel assignĂ©' : 'Equipment Assigned', target: r.equipment?.name || equipMap.get(r.equipment_id) || 'IT Asset', user: r.user ? `${r.user.first_name} ${r.user.last_name}` : 'Employee', time: formatTime(r.created_at || new Date().toISOString()), @@ -155,7 +165,7 @@ export const Dashboard: React.FC = () => { maintenances.slice(0, 2).forEach(m => { recentList.push({ id: m.id, - action: `Maintenance Ticket`, + action: isFr ? 'Ticket de maintenance' : 'Maintenance Ticket', target: m.equipment?.name || equipMap.get(m.equipment_id) || 'IT Asset', user: m.requested_by ? `${m.requested_by.first_name} ${m.requested_by.last_name}` : 'Requester', time: formatTime(m.created_at || new Date().toISOString()), @@ -175,12 +185,14 @@ export const Dashboard: React.FC = () => { }; fetchDashboardData(); - }, [role]); + }, [role, language]); if (isLoading) { return (
-
Loading dashboard metrics...
+
+ {isFr ? 'Chargement des statistiques...' : 'Loading dashboard metrics...'} +
); } @@ -189,9 +201,11 @@ export const Dashboard: React.FC = () => {
-

Dashboard

+

+ {isFr ? 'Tableau de bord' : 'Dashboard'} +

- Overview of your IT Asset Management environment. + {isFr ? 'Aperçu général de la gestion de votre parc informatique.' : 'Overview of your IT Asset Management environment.'}

@@ -199,18 +213,18 @@ export const Dashboard: React.FC = () => { <> )} {(role === 'Admin' || role === 'Manager') && ( )}
@@ -220,49 +234,65 @@ export const Dashboard: React.FC = () => {
- Total Assets + + {isFr ? 'Total Équipements' : 'Total Assets'} +
{stats.totalAssets}
-

Active hardware in database

+

+ {isFr ? 'Matériel actif enregistré' : 'Active hardware in database'} +

- Available Gear + + {isFr ? 'Équipements disponibles' : 'Available Gear'} +
{stats.availableGear}

{stats.totalAssets > 0 - ? `${Math.round((stats.availableGear / stats.totalAssets) * 100)}% of total inventory` - : '0% of total inventory'} + ? isFr + ? `${Math.round((stats.availableGear / stats.totalAssets) * 100)}% de l'inventaire total` + : `${Math.round((stats.availableGear / stats.totalAssets) * 100)}% of total inventory` + : '0%'}

- Active Reservations + + {isFr ? 'Réservations Actives' : 'Active Reservations'} +
{stats.activeReservations}
-

Currently checked out by users

+

+ {isFr ? 'Actuellement assignés aux utilisateurs' : 'Currently checked out by users'} +

- Critical Maintenance + + {isFr ? 'Maintenance Critique' : 'Critical Maintenance'} +
{stats.criticalMaintenances}
-

Requires immediate action

+

+ {isFr ? 'Nécessite une action immédiate' : 'Requires immediate action'} +

@@ -272,11 +302,15 @@ export const Dashboard: React.FC = () => { {/* Recent Activity */} - Recent Activity + + {isFr ? 'Activité Récente' : 'Recent Activity'} + {activities.length === 0 ? ( -
No recent activities found.
+
+ {isFr ? 'Aucune activité récente trouvée.' : 'No recent activities found.'} +
) : (
{activities.map((activity) => ( @@ -301,7 +335,9 @@ export const Dashboard: React.FC = () => { {/* Quick Links / Notifications */} - System Health + + {isFr ? 'État du systùme' : 'System Health'} +
@@ -312,8 +348,12 @@ export const Dashboard: React.FC = () => {
-

All Systems Operational

-

API, Database, and Storage are running smoothly.

+

+ {isFr ? 'Tous les systÚmes sont opérationnels' : 'All Systems Operational'} +

+

+ {isFr ? 'L\'API, la base de données et les serveurs fonctionnent normalement.' : 'API, Database, and Storage are running smoothly.'} +

diff --git a/frontend/src/pages/Departments.tsx b/frontend/src/pages/Departments.tsx index 1f2a0e6..f7de1ce 100644 --- a/frontend/src/pages/Departments.tsx +++ b/frontend/src/pages/Departments.tsx @@ -20,9 +20,12 @@ import type { Department } from '@/types'; import { Building2, Plus, Edit, Trash2 } from 'lucide-react'; import { apiClient } from '@/api/apiClient'; import { useAuth } from '@/context/AuthContext'; +import { useTranslation } from '@/context/LanguageContext'; export const Departments: React.FC = () => { const { role } = useAuth(); + const { language } = useTranslation(); + const isFr = language === 'fr'; const [departments, setDepartments] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isFormOpen, setIsFormOpen] = useState(false); @@ -74,12 +77,12 @@ export const Departments: React.FC = () => { }; const handleDelete = async (id: string) => { - if (!window.confirm('Delete this department?')) return; + if (!window.confirm(isFr ? 'Supprimer ce dĂ©partement ?' : 'Delete this department?')) return; try { await apiClient.delete(`/department/${id}`); fetchDepartments(); } catch (err: any) { - alert(err.response?.data?.message || 'Failed to delete department.'); + alert(err.response?.data?.message || (isFr ? 'Échec de la suppression.' : 'Failed to delete department.')); } }; @@ -87,7 +90,7 @@ export const Departments: React.FC = () => { e.preventDefault(); setErrorMessage(''); if (!formData.name) { - setErrorMessage('Name is required.'); + setErrorMessage(isFr ? 'Le nom est obligatoire.' : 'Name is required.'); return; } @@ -105,7 +108,7 @@ export const Departments: React.FC = () => { setIsFormOpen(false); fetchDepartments(); } catch (err: any) { - setErrorMessage(err.response?.data?.message || 'An error occurred.'); + setErrorMessage(err.response?.data?.message || (isFr ? 'Une erreur est survenue.' : 'An error occurred.')); } }; @@ -113,27 +116,33 @@ export const Departments: React.FC = () => {
-

Departments

-

Manage corporate departments and organizational structures.

+

+ {isFr ? 'Départements' : 'Departments'} +

+

+ {isFr ? 'Gérez les départements de l\'entreprise et la structure organisationnelle.' : 'Manage corporate departments and organizational structures.'} +

{role === 'Admin' && ( - )}
{isLoading ? (
-
Loading departments...
+
+ {isFr ? 'Chargement des départements...' : 'Loading departments...'} +
) : (
- Department Name + {isFr ? 'Nom du département' : 'Department Name'} Description {role === 'Admin' && Actions} @@ -142,7 +151,7 @@ export const Departments: React.FC = () => { {departments.length === 0 ? ( - No departments found. + {isFr ? 'Aucun département trouvé.' : 'No departments found.'} ) : ( @@ -156,7 +165,7 @@ export const Departments: React.FC = () => { {dep.name} - {dep.description || 'No description provided.'} + {dep.description || (isFr ? 'Aucune description fournie.' : 'No description provided.')} {role === 'Admin' && (
@@ -164,19 +173,19 @@ export const Departments: React.FC = () => { onClick={() => openEditForm(dep)} size="sm" variant="outline" - className="border-border hover:bg-secondary flex items-center gap-1" + className="border-border hover:bg-secondary flex items-center gap-1 cursor-pointer" > - Edit + {isFr ? 'Modifier' : 'Edit'}
@@ -194,10 +203,12 @@ export const Departments: React.FC = () => { - {isEditing ? 'Modify Department' : 'Create Department'} + {isEditing + ? isFr ? 'Modifier le département' : 'Modify Department' + : isFr ? 'Créer le département' : 'Create Department'} - Provide organizational details for the department. + {isFr ? 'Fournissez les détails organisationnels pour le département.' : 'Provide organizational details for the department.'} @@ -209,7 +220,9 @@ export const Departments: React.FC = () => {
- + {
@@ -111,9 +174,9 @@ export const SelfService: React.FC = () => { {step === 3 && ( - )} @@ -123,36 +186,48 @@ export const SelfService: React.FC = () => {
{/* Kanban Columns */} - {['Pending', 'In Progress', 'Resolved'].map((col, index) => ( -
-
-

- {index === 0 && } - {index === 1 && } - {index === 2 && } - {col} -

- {index === 0 ? 2 : 1} + {['Pending', 'In Progress', 'Resolved'].map((col, index) => { + let displayCol = col; + if (isFr) { + if (col === 'Pending') displayCol = 'En attente'; + else if (col === 'In Progress') displayCol = 'En cours'; + else if (col === 'Resolved') displayCol = 'Résolu'; + } + return ( +
+
+

+ {index === 0 && } + {index === 1 && } + {index === 2 && } + {displayCol} +

+ {index === 0 ? 2 : 1} +
+ + {/* Mock Ticket Card */} + {index === 0 && ( + + +
+ + {isFr ? 'Critique' : 'Critical'} + + #TKT-892 +
+

+ {isFr ? 'L\'écran clignote de maniÚre aléatoire' : 'Screen flickering randomly'} +

+
+ + Dell UltraSharp 32" +
+
+
+ )}
- - {/* Mock Ticket Card */} - {index === 0 && ( - - -
- Critical - #TKT-892 -
-

Screen flickering randomly

-
- - Dell UltraSharp 32" -
-
-
- )} -
- ))} + ); + })}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..2c9c208 --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,733 @@ +import React, { useEffect, useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { useAuth } from '@/context/AuthContext'; +import { useTranslation } from '@/context/LanguageContext'; +import { apiClient } from '@/api/apiClient'; +import type { Department } from '@/types'; +import { useNavigate } from 'react-router-dom'; +import { + User, + Lock, + Mail, + Phone, + Image as ImageIcon, + CheckCircle, + AlertCircle, + Settings as SettingsIcon, + Palette, + Globe, + Bell, + Monitor, + UploadCloud +} from 'lucide-react'; + +interface SettingsProps { + view?: 'profile' | 'system'; +} + +export const Settings: React.FC = ({ view = 'profile' }) => { + const { user, refreshUser } = useAuth(); + const { language, setLanguage, t } = useTranslation(); + const navigate = useNavigate(); + + // Profile form state + const [profileData, setProfileData] = useState({ + first_name: '', + last_name: '', + phone_number: '', + profile_image: '', + }); + + // Password form state + const [passwordData, setPasswordData] = useState({ + old_password: '', + new_password: '', + confirm_password: '', + }); + + // System Settings state + const [systemTheme, setSystemTheme] = useState(() => { + return localStorage.getItem('system_theme') || 'dark'; + }); + const [notificationsEnabled, setNotificationsEnabled] = useState(() => { + return localStorage.getItem('system_notifications') !== 'false'; + }); + const [debugLogsEnabled, setDebugLogsEnabled] = useState(() => { + return localStorage.getItem('system_debug_logs') === 'true'; + }); + + // Departments list for display mapping + const [departments, setDepartments] = useState([]); + + // Feedback states + const [profileSuccess, setProfileSuccess] = useState(''); + const [profileError, setProfileError] = useState(''); + const [passwordSuccess, setPasswordSuccess] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [systemSuccess, setSystemSuccess] = useState(''); + const [isSavingProfile, setIsSavingProfile] = useState(false); + const [isSavingPassword, setIsSavingPassword] = useState(false); + const [imageSourceType, setImageSourceType] = useState<'upload' | 'url'>('upload'); + const [isUploading, setIsUploading] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files?.[0]; + if (file) { + await uploadProfileFile(file); + } + }; + + const uploadProfileFile = async (file: File) => { + setIsUploading(true); + const uploadData = new FormData(); + uploadData.append('file', file); + uploadData.append('folder', 'Users'); + + try { + const response = await apiClient.post<{ image_url: string; filename: string }>('/upload', uploadData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + setProfileData(prev => ({ + ...prev, + profile_image: response.data.image_url + })); + } catch (err: any) { + console.error('Upload failed', err); + alert(language === 'fr' ? 'Le tĂ©lĂ©chargement de l\'image a Ă©chouĂ©.' : 'Image upload failed.'); + } finally { + setIsUploading(false); + } + }; + + const handleProfileFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + await uploadProfileFile(file); + } + }; + + useEffect(() => { + // Load departments + const fetchDeps = async () => { + try { + const res = await apiClient.get('/department'); + setDepartments(res.data || []); + } catch (err) { + console.error('Failed to load departments', err); + } + }; + fetchDeps(); + }, []); + + useEffect(() => { + if (user) { + setProfileData({ + first_name: user.first_name || '', + last_name: user.last_name || '', + phone_number: user.phone_number || '', + profile_image: user.profile_image || '', + }); + if (user.profile_image && !user.profile_image.startsWith('http')) { + setImageSourceType('upload'); + } else { + setImageSourceType('url'); + } + } + }, [user]); + + const handleProfileSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setProfileSuccess(''); + setProfileError(''); + + if (!user) return; + + if (!profileData.first_name.trim() || !profileData.last_name.trim()) { + setProfileError(language === 'fr' ? 'Le prĂ©nom et le nom sont requis.' : 'First Name and Last Name are required.'); + return; + } + + setIsSavingProfile(true); + try { + const payload = { + ...user, + first_name: profileData.first_name.trim(), + last_name: profileData.last_name.trim(), + phone_number: profileData.phone_number.trim() || null, + profile_image: profileData.profile_image.trim() || null, + }; + + await apiClient.put(`/user/${user.id}`, payload); + await refreshUser(); + setProfileSuccess(t('profileSuccess')); + } catch (err: any) { + setProfileError(err.response?.data?.message || (language === 'fr' ? 'Échec de la mise Ă  jour.' : 'Failed to update profile details.')); + } finally { + setIsSavingProfile(false); + } + }; + + const handlePasswordSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setPasswordSuccess(''); + setPasswordError(''); + + if (!user) return; + + if (!passwordData.old_password) { + setPasswordError(language === 'fr' ? 'Le mot de passe actuel est requis.' : 'Current password is required.'); + return; + } + + if (!passwordData.new_password) { + setPasswordError(language === 'fr' ? 'Le nouveau mot de passe ne peut pas ĂȘtre vide.' : 'New password cannot be empty.'); + return; + } + + if (passwordData.new_password !== passwordData.confirm_password) { + setPasswordError(language === 'fr' ? 'Les nouveaux mots de passe ne correspondent pas.' : 'New passwords do not match.'); + return; + } + + setIsSavingPassword(true); + try { + const payload = { + old_password: passwordData.old_password, + new_password: passwordData.new_password, + }; + + await apiClient.put(`/user/${user.id}/change-password`, payload); + setPasswordSuccess(t('passwordSuccess')); + setPasswordData({ + old_password: '', + new_password: '', + confirm_password: '', + }); + } catch (err: any) { + setPasswordError(err.response?.data?.message || (language === 'fr' ? 'Échec du changement de mot de passe. Assurez-vous que le mot de passe actuel est correct.' : 'Failed to change password. Make sure current password is correct.')); + } finally { + setIsSavingPassword(false); + } + }; + + const handleThemeChange = (theme: string) => { + setSystemTheme(theme); + localStorage.setItem('system_theme', theme); + + const root = document.documentElement; + root.className = ''; + if (theme === 'dark') { + root.classList.add('dark'); + } else if (theme === 'cyberpunk') { + root.classList.add('dark', 'theme-cyberpunk'); + } + + setSystemSuccess(t('themeSuccess')); + setTimeout(() => setSystemSuccess(''), 3000); + }; + + const handleLanguageChange = (lang: string) => { + setLanguage(lang as 'en' | 'fr'); + setSystemSuccess(lang === 'fr' ? 'PrĂ©fĂ©rence de langue enregistrĂ©e !' : 'System language preference saved!'); + setTimeout(() => setSystemSuccess(''), 3000); + }; + + const toggleNotifications = () => { + const val = !notificationsEnabled; + setNotificationsEnabled(val); + localStorage.setItem('system_notifications', String(val)); + setSystemSuccess(val ? t('notificationsEnabled') : t('notificationsDisabled')); + setTimeout(() => setSystemSuccess(''), 3000); + }; + + const toggleDebugLogs = () => { + const val = !debugLogsEnabled; + setDebugLogsEnabled(val); + localStorage.setItem('system_debug_logs', String(val)); + setSystemSuccess(val ? t('devLogsEnabled') : t('devLogsDisabled')); + setTimeout(() => setSystemSuccess(''), 3000); + }; + + const depName = user?.department?.name || departments.find(d => d.id === user?.department_id)?.name || t('none'); + + return ( +
+
+

{t('settings')}

+

{t('settingsDesc')}

+
+ + {/* Tabs Menu */} +
+ + +
+ +
+ {/* Left Card - Quick Details Overview */} +
+ +
+ +
+ {user?.profile_image_url ? ( + Profile + ) : ( + {user?.first_name ? user.first_name[0].toUpperCase() : 'U'} + )} +
+

+ {user ? `${user.first_name} ${user.last_name}` : 'Loading User...'} +

+

+ + {user?.email} +

+ +
+
+ {t('accessRole')} + + {user?.role} + +
+
+ {t('department')} + {depName} +
+
+ {t('status')} + + {user?.is_active ? t('activeAccount') : t('inactiveAccount')} + +
+
+
+ +
+ + {/* Right Columns - Views */} +
+ {view === 'profile' ? ( + <> + {/* Profile Details Form */} + + + + + {t('personalProfile')} + + {t('personalProfileDesc')} + + + {profileSuccess && ( +
+ + {profileSuccess} +
+ )} + {profileError && ( +
+ + {profileError} +
+ )} + + +
+
+ + setProfileData({ ...profileData, first_name: e.target.value })} + placeholder={t('firstName')} + className="w-full px-3.5 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+
+ + setProfileData({ ...profileData, last_name: e.target.value })} + placeholder={t('lastName')} + className="w-full px-3.5 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+
+ +
+ +
+ + setProfileData({ ...profileData, phone_number: e.target.value })} + placeholder="+33 6 12 34 56 78" + className="w-full pl-11 pr-4 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+
+ +
+ +
+ + +
+ + {imageSourceType === 'upload' ? ( +
+
document.getElementById('profile-file-upload-input')?.click()} + className={`relative border border-dashed rounded-xl p-6 flex flex-col items-center justify-center gap-3 transition-all cursor-pointer ${ + isDragging + ? 'border-primary bg-primary/5 scale-[0.99]' + : 'border-border bg-secondary/10 hover:bg-secondary/20 hover:border-primary/40' + }`} + > + + +
+ +
+ +
+

+ {language === 'fr' ? 'Glissez-déposez votre avatar ici' : 'Drag & drop your avatar here'} +

+

+ {language === 'fr' ? 'ou cliquez pour parcourir vos fichiers' : 'or click to browse your files'} +

+
+ + {isUploading && ( +
+ +
+ )} +
+ + {profileData.profile_image && ( +
+ Preview +
+ )} +
+ ) : ( +
+ + setProfileData({ ...profileData, profile_image: e.target.value })} + placeholder="mon_avatar.png" + className="w-full pl-11 pr-4 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+ )} + {t('profileImageHint')} +
+ +
+ +
+ +
+
+ + {/* Security & Password Form */} + + + + + {t('securityPassword')} + + {t('securityPasswordDesc')} + + + {passwordSuccess && ( +
+ + {passwordSuccess} +
+ )} + {passwordError && ( +
+ + {passwordError} +
+ )} + +
+
+ + setPasswordData({ ...passwordData, old_password: e.target.value })} + placeholder="‱‱‱‱‱‱‱‱" + className="w-full px-3.5 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+ +
+
+ + setPasswordData({ ...passwordData, new_password: e.target.value })} + placeholder="‱‱‱‱‱‱‱‱" + className="w-full px-3.5 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+
+ + setPasswordData({ ...passwordData, confirm_password: e.target.value })} + placeholder="‱‱‱‱‱‱‱‱" + className="w-full px-3.5 py-2.5 rounded-xl border border-border bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/60 focus:border-transparent transition-all" + /> +
+
+ +
+ +
+ +
+
+ + ) : ( + /* System Settings View */ + + + + + {t('systemPreferences')} + + {t('systemPreferencesDesc')} + + + {systemSuccess && ( +
+ + {systemSuccess} +
+ )} + + {/* Theme Mode Selector */} +
+
+ +

{t('systemInterfaceTheme')}

+
+
+ {/* Dark Theme */} + + + {/* Light Theme */} + + + {/* Cyberpunk Theme */} + +
+
+ +
+ {/* Language Selector */} +
+
+
+ +

{t('localizationLanguage')}

+
+

{t('localizationLanguageDesc')}

+
+ +
+ + {/* Desktop Notifications Toggle */} +
+
+
+ +

{t('systemNotifications')}

+
+

{t('systemNotificationsDesc')}

+
+ +
+ + {/* Audit Logs / Debug Mode Toggle */} +
+
+
+ +

{t('developerLogs')}

+
+

{t('developerLogsDesc')}

+
+ +
+
+
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index 81dae92..783990b 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -21,9 +21,12 @@ import type { User, Department, UserRole } from '@/types'; import { Users as UsersIcon, Plus, Edit, Trash2 } from 'lucide-react'; import { apiClient } from '@/api/apiClient'; import { useAuth } from '@/context/AuthContext'; +import { useTranslation } from '@/context/LanguageContext'; export const Users: React.FC = () => { const { role } = useAuth(); + const { language } = useTranslation(); + const isFr = language === 'fr'; const [users, setUsers] = useState([]); const [departments, setDepartments] = useState([]); @@ -74,11 +77,11 @@ export const Users: React.FC = () => { const getRoleBadge = (userRole: UserRole) => { switch (userRole) { case 'Admin': - return Admin; + return {isFr ? 'Administrateur' : 'Admin'}; case 'Manager': - return Manager; + return {isFr ? 'Gestionnaire' : 'Manager'}; case 'Employee': - return Employee; + return {isFr ? 'EmployĂ©' : 'Employee'}; default: return {userRole}; } @@ -117,12 +120,12 @@ export const Users: React.FC = () => { }; const handleDelete = async (id: string) => { - if (!window.confirm('Delete this user account?')) return; + if (!window.confirm(isFr ? 'Supprimer ce compte utilisateur ?' : 'Delete this user account?')) return; try { await apiClient.delete(`/user/${id}`); fetchUsers(); } catch (err: any) { - alert(err.response?.data?.message || 'Failed to delete user.'); + alert(err.response?.data?.message || (isFr ? 'Échec de la suppression.' : 'Failed to delete user.')); } }; @@ -130,7 +133,7 @@ export const Users: React.FC = () => { e.preventDefault(); setErrorMessage(''); if (!formData.first_name || !formData.last_name || !formData.email) { - setErrorMessage('First name, Last name and Email are required.'); + setErrorMessage(isFr ? 'Le prĂ©nom, le nom et l\'adresse e-mail sont requis.' : 'First name, Last name and Email are required.'); return; } @@ -150,7 +153,7 @@ export const Users: React.FC = () => { setIsFormOpen(false); fetchUsers(); } catch (err: any) { - setErrorMessage(err.response?.data?.message || 'An error occurred.'); + setErrorMessage(err.response?.data?.message || (isFr ? 'Une erreur est survenue.' : 'An error occurred.')); } }; @@ -158,31 +161,37 @@ export const Users: React.FC = () => {
-

Users

-

Manage user access rights, roles, and corporate departments.

+

+ {isFr ? 'Utilisateurs' : 'Users'} +

+

+ {isFr ? 'Gérez les droits d\'accÚs des utilisateurs, leurs rÎles et leurs départements.' : 'Manage user access rights, roles, and corporate departments.'} +

{role === 'Admin' && ( - )}
{isLoading ? (
-
Loading directory...
+
+ {isFr ? 'Chargement de l\'annuaire...' : 'Loading directory...'} +
) : (
- Full Name - Email - Role - Department - Status + {isFr ? 'Nom complet' : 'Full Name'} + {isFr ? 'E-mail' : 'Email'} + {isFr ? 'RÎle' : 'Role'} + {isFr ? 'Département' : 'Department'} + {isFr ? 'Statut' : 'Status'} {role === 'Admin' && Actions} @@ -190,12 +199,12 @@ export const Users: React.FC = () => { {users.length === 0 ? ( - No users found. + {isFr ? 'Aucun utilisateur trouvé.' : 'No users found.'} ) : ( users.map((usr) => { - const depName = usr.department?.name || departments.find(d => d.id === usr.department_id)?.name || 'None'; + const depName = usr.department?.name || departments.find(d => d.id === usr.department_id)?.name || (isFr ? 'Aucun' : 'None'); return ( @@ -212,7 +221,7 @@ export const Users: React.FC = () => { {depName} - {usr.is_active ? 'Active' : 'Inactive'} + {usr.is_active ? (isFr ? 'Actif' : 'Active') : (isFr ? 'Inactif' : 'Inactive')} {role === 'Admin' && ( @@ -222,19 +231,19 @@ export const Users: React.FC = () => { onClick={() => openEditForm(usr)} size="sm" variant="outline" - className="border-border hover:bg-secondary flex items-center gap-1" + className="border-border hover:bg-secondary flex items-center gap-1 cursor-pointer" > - Edit + {isFr ? 'Modifier' : 'Edit'} @@ -253,10 +262,14 @@ export const Users: React.FC = () => { - {isEditing ? 'Modify Account' : 'Create Account'} + {isEditing + ? isFr ? 'Modifier le compte' : 'Modify Account' + : isFr ? 'Créer le compte' : 'Create Account'} - {isEditing ? 'Update profile and roles.' : 'Add new team member. Default password is Password123!'} + {isEditing + ? isFr ? 'Mettez à jour le profil et les rÎles.' : 'Update profile and roles.' + : isFr ? 'Ajouter un nouveau membre. Le mot de passe par défaut est Password123!' : 'Add new team member. Default password is Password123!'} @@ -269,7 +282,9 @@ export const Users: React.FC = () => {
- + {
- + {
- + {
- + {
- +
- +