# Expo Tasks & Etapes Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add task and etape (stage) management to the Expo mobile app, with a tablo detail screen, task list grouped by etape, task CRUD, and etape CRUD — all backed by the existing Supabase database.

**Architecture:** New tablo detail screen as entry point from tablo list. Tasks fetched directly from Supabase `tasks_with_assignee` view. Etapes are tasks with `is_parent: true`. New React Query hooks mirror the web app pattern. Components use existing theme/styling conventions (useThemeColor, Modal, FlatList with RefreshControl).

**Tech Stack:** Expo Router (file-based routing), React Query v5, Supabase JS client, React Native (Modal, FlatList, Alert), lucide-react-native icons, expo DateTimePicker.

---

## File Structure

### Types
- **Create:** `xtablo-expo/types/tasks.types.ts` — Task, Etape, TaskInsert, TaskUpdate, TaskStatus types

### Hooks
- **Create:** `xtablo-expo/hooks/tasks.ts` — useTasksByTablo, useCreateTask, useUpdateTask, useDeleteTask
- **Create:** `xtablo-expo/hooks/etapes.ts` — useTabloEtapes, useCreateEtape, useUpdateEtape, useDeleteEtape
- **Create:** `xtablo-expo/hooks/members.ts` — useTabloMembers (read-only)

### Screens
- **Create:** `xtablo-expo/app/(app)/tablo/_layout.tsx` — Stack layout for tablo routes
- **Create:** `xtablo-expo/app/(app)/tablo/[id].tsx` — Tablo detail screen with task list
- **Create:** `xtablo-expo/app/(app)/task/_layout.tsx` — Stack layout for task routes
- **Create:** `xtablo-expo/app/(app)/task/[id].tsx` — Task create/edit screen

### Components
- **Create:** `xtablo-expo/components/tasks/TaskList.tsx` — SectionList with etape sections
- **Create:** `xtablo-expo/components/tasks/TaskRow.tsx` — Single task row
- **Create:** `xtablo-expo/components/tasks/EtapeSection.tsx` — Collapsible etape header
- **Create:** `xtablo-expo/components/tasks/EtapeSheet.tsx` — Modal for etape create/edit
- **Create:** `xtablo-expo/components/tasks/StatusControl.tsx` — Segmented status picker
- **Create:** `xtablo-expo/components/tasks/AssigneePicker.tsx` — Modal for assignee selection
- **Create:** `xtablo-expo/components/tasks/EtapePicker.tsx` — Modal for etape selection

### Modified Files
- **Modify:** `xtablo-expo/types/database.types.ts` — Add tasks table, tasks_with_assignee view, task_status enum
- **Modify:** `xtablo-expo/app/(app)/_layout.tsx` — Register tablo and task Stack.Screens
- **Modify:** `xtablo-expo/app/(app)/(tabs)/tablos.tsx` — Navigate to tablo detail on tap instead of channel

---

### Task 1: Update Database Types

**Files:**
- Modify: `xtablo-expo/types/database.types.ts`

The Expo app's database types file is outdated — it has no `tasks` table, no `tasks_with_assignee` view, and no `task_status` enum. We need to add them.

- [ ] **Step 1: Add task_status enum to Enums section**

Open `xtablo-expo/types/database.types.ts`. Find the `Enums` section (line ~360) which currently only has `devis_status`. Add `task_status` after it:

```typescript
    Enums: {
      devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired";
      task_status: "todo" | "in_progress" | "in_review" | "done";
    };
```

- [ ] **Step 2: Add tasks table to Tables section**

In the `Tables` section (after the last existing table, before the closing `};` of Tables), add:

```typescript
      tasks: {
        Row: {
          assignee_id: string | null;
          created_at: string;
          deleted_at: string | null;
          description: string | null;
          due_date: string | null;
          id: string;
          is_parent: boolean;
          parent_task_id: string | null;
          position: number;
          status: Database["public"]["Enums"]["task_status"];
          tablo_id: string;
          title: string;
          updated_at: string;
        };
        Insert: {
          assignee_id?: string | null;
          created_at?: string;
          deleted_at?: string | null;
          description?: string | null;
          due_date?: string | null;
          id?: string;
          is_parent?: boolean;
          parent_task_id?: string | null;
          position?: number;
          status?: Database["public"]["Enums"]["task_status"];
          tablo_id: string;
          title: string;
          updated_at?: string;
        };
        Update: {
          assignee_id?: string | null;
          created_at?: string;
          deleted_at?: string | null;
          description?: string | null;
          due_date?: string | null;
          id?: string;
          is_parent?: boolean;
          parent_task_id?: string | null;
          position?: number;
          status?: Database["public"]["Enums"]["task_status"];
          tablo_id?: string;
          title?: string;
          updated_at?: string;
        };
        Relationships: [];
      };
```

- [ ] **Step 3: Add tasks_with_assignee view to Views section**

In the `Views` section, add:

```typescript
      tasks_with_assignee: {
        Row: {
          assignee_avatar: string | null;
          assignee_id: string | null;
          assignee_name: string | null;
          created_at: string | null;
          description: string | null;
          due_date: string | null;
          id: string | null;
          is_parent: boolean | null;
          parent_task_id: string | null;
          position: number | null;
          status: Database["public"]["Enums"]["task_status"] | null;
          tablo_id: string | null;
          title: string | null;
          updated_at: string | null;
        };
        Relationships: [];
      };
```

- [ ] **Step 4: Verify types compile**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | head -20`
Expected: No errors related to tasks types (pre-existing errors are fine).

- [ ] **Step 5: Commit**

```bash
git add xtablo-expo/types/database.types.ts
git commit -m "feat(expo): add tasks and tasks_with_assignee database types"
```

---

### Task 2: Create Task and Etape Types

**Files:**
- Create: `xtablo-expo/types/tasks.types.ts`

- [ ] **Step 1: Create the types file**

Create `xtablo-expo/types/tasks.types.ts`:

```typescript
import { Database, Tables, TablesInsert, TablesUpdate } from "@/types/database.types";
import { RemoveNullFromObject } from "@/types/removeNull";

export type TaskStatus = Database["public"]["Enums"]["task_status"];

export type Task = RemoveNullFromObject<
  Tables<"tasks_with_assignee">,
  "id" | "tablo_id" | "is_parent" | "title" | "status" | "position" | "created_at" | "updated_at"
>;

export type Etape = RemoveNullFromObject<
  Tables<"tasks">,
  "id" | "tablo_id" | "title" | "is_parent"
>;

export type TaskInsert = TablesInsert<"tasks">;
export type TaskUpdate = TablesUpdate<"tasks">;

export const TASK_STATUSES: { value: TaskStatus; label: string; color: string }[] = [
  { value: "todo", label: "À faire", color: "#3b82f6" },
  { value: "in_progress", label: "En cours", color: "#eab308" },
  { value: "in_review", label: "Vérification", color: "#a855f7" },
  { value: "done", label: "Terminé", color: "#22c55e" },
];
```

- [ ] **Step 2: Verify types compile**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | grep tasks`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add xtablo-expo/types/tasks.types.ts
git commit -m "feat(expo): add task and etape type definitions"
```

---

### Task 3: Create Task Hooks

**Files:**
- Create: `xtablo-expo/hooks/tasks.ts`

- [ ] **Step 1: Create the hooks file**

Create `xtablo-expo/hooks/tasks.ts`:

```typescript
import { supabase } from "@/lib/supabase";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Task, TaskInsert, TaskUpdate } from "@/types/tasks.types";
import { Alert } from "react-native";

export const useTasksByTablo = (tabloId: string | undefined) => {
  return useQuery({
    queryKey: ["tasks", "tablo", tabloId],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("tasks_with_assignee")
        .select("*")
        .eq("tablo_id", tabloId!)
        .eq("is_parent", false)
        .order("position", { ascending: true });

      if (error) throw error;
      return data as Task[];
    },
    enabled: !!tabloId,
  });
};

export const useCreateTask = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (task: TaskInsert) => {
      const { data, error } = await supabase
        .from("tasks")
        .insert({
          tablo_id: task.tablo_id,
          title: task.title,
          description: task.description ?? null,
          status: task.status || "todo",
          assignee_id: task.assignee_id ?? null,
          position: task.position ?? 0,
          parent_task_id: task.parent_task_id ?? null,
          is_parent: false,
          due_date: task.due_date ?? null,
        })
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tablo_id] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de créer la tâche.");
    },
  });
};

export const useUpdateTask = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, tabloId, ...updates }: TaskUpdate & { id: string; tabloId: string }) => {
      const { data, error } = await supabase
        .from("tasks")
        .update(updates)
        .eq("id", id)
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tabloId] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de modifier la tâche.");
    },
  });
};

export const useDeleteTask = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, tabloId }: { id: string; tabloId: string }) => {
      const { error } = await supabase.from("tasks").delete().eq("id", id);
      if (error) throw error;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tabloId] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de supprimer la tâche.");
    },
  });
};
```

- [ ] **Step 2: Verify types compile**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | grep "hooks/tasks"`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add xtablo-expo/hooks/tasks.ts
git commit -m "feat(expo): add task CRUD hooks"
```

---

### Task 4: Create Etape Hooks

**Files:**
- Create: `xtablo-expo/hooks/etapes.ts`

- [ ] **Step 1: Create the hooks file**

Create `xtablo-expo/hooks/etapes.ts`:

```typescript
import { supabase } from "@/lib/supabase";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Etape } from "@/types/tasks.types";
import { Alert } from "react-native";

export const useTabloEtapes = (tabloId: string | undefined) => {
  return useQuery({
    queryKey: ["tablo-etapes", tabloId],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("tasks")
        .select("*")
        .eq("tablo_id", tabloId!)
        .eq("is_parent", true)
        .is("deleted_at", null)
        .order("position", { ascending: true });

      if (error) throw error;
      return data as Etape[];
    },
    enabled: !!tabloId,
  });
};

export const useCreateEtape = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ tabloId, title, description, position, due_date }: {
      tabloId: string;
      title: string;
      description?: string | null;
      position?: number;
      due_date?: string | null;
    }) => {
      const { data, error } = await supabase
        .from("tasks")
        .insert({
          tablo_id: tabloId,
          title,
          description: description ?? null,
          position: position ?? 0,
          is_parent: true,
          status: "todo",
          due_date: due_date ?? null,
        })
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tablo-etapes", variables.tabloId] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de créer l'étape.");
    },
  });
};

export const useUpdateEtape = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, tabloId, ...updates }: {
      id: string;
      tabloId: string;
      title?: string;
      description?: string | null;
      position?: number;
      due_date?: string | null;
    }) => {
      const { data, error } = await supabase
        .from("tasks")
        .update(updates)
        .eq("id", id)
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tablo-etapes", variables.tabloId] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de modifier l'étape.");
    },
  });
};

export const useDeleteEtape = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, tabloId }: { id: string; tabloId: string }) => {
      const { error } = await supabase.from("tasks").delete().eq("id", id);
      if (error) throw error;
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["tablo-etapes", variables.tabloId] });
      queryClient.invalidateQueries({ queryKey: ["tasks", "tablo", variables.tabloId] });
    },
    onError: () => {
      Alert.alert("Erreur", "Impossible de supprimer l'étape.");
    },
  });
};
```

- [ ] **Step 2: Verify types compile**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | grep "hooks/etapes"`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add xtablo-expo/hooks/etapes.ts
git commit -m "feat(expo): add etape CRUD hooks"
```

---

### Task 5: Create Members Hook

**Files:**
- Create: `xtablo-expo/hooks/members.ts`

- [ ] **Step 1: Create the hook file**

The web app fetches members via REST API at `/api/v1/tablos/members/:tabloId`. The Expo app should do the same.

Create `xtablo-expo/hooks/members.ts`:

```typescript
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { useAuthStore } from "@/stores/auth";

export type TabloMember = {
  id: string;
  name: string;
  is_admin: boolean;
  email: string;
  avatar_url: string | null;
};

export const useTabloMembers = (tabloId: string | undefined) => {
  const session = useAuthStore((state) => state.session);

  return useQuery({
    queryKey: ["tablo-members", tabloId],
    queryFn: async () => {
      const { data } = await api.get<{ members: TabloMember[] }>(
        `/api/v1/tablos/members/${tabloId}`,
        {
          headers: {
            Authorization: `Bearer ${session?.access_token}`,
          },
        }
      );
      return data.members;
    },
    enabled: !!tabloId && !!session,
  });
};
```

- [ ] **Step 2: Verify types compile**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | grep "hooks/members"`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add xtablo-expo/hooks/members.ts
git commit -m "feat(expo): add tablo members read-only hook"
```

---

### Task 6: Create StatusControl Component

**Files:**
- Create: `xtablo-expo/components/tasks/StatusControl.tsx`

- [ ] **Step 1: Create the component**

Create `xtablo-expo/components/tasks/StatusControl.tsx`:

```typescript
import React from "react";
import { View, TouchableOpacity, Text, StyleSheet } from "react-native";
import { TaskStatus, TASK_STATUSES } from "@/types/tasks.types";
import { useThemeColor } from "@/hooks/useThemeColor";

type StatusControlProps = {
  value: TaskStatus;
  onChange: (status: TaskStatus) => void;
};

export default function StatusControl({ value, onChange }: StatusControlProps) {
  const bgColor = useThemeColor({ light: "#f1f5f9", dark: "#1f2937" }, "background");

  return (
    <View style={[styles.container, { backgroundColor: bgColor }]}>
      {TASK_STATUSES.map((status) => {
        const isActive = value === status.value;
        return (
          <TouchableOpacity
            key={status.value}
            style={[
              styles.segment,
              isActive && { backgroundColor: status.color },
            ]}
            onPress={() => onChange(status.value)}
          >
            <Text
              style={[
                styles.label,
                { color: isActive ? "#ffffff" : "#9ca3af" },
              ]}
              numberOfLines={1}
            >
              {status.label}
            </Text>
          </TouchableOpacity>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    borderRadius: 10,
    padding: 3,
    gap: 2,
  },
  segment: {
    flex: 1,
    paddingVertical: 8,
    borderRadius: 8,
    alignItems: "center",
  },
  label: {
    fontSize: 11,
    fontWeight: "600",
  },
});
```

- [ ] **Step 2: Commit**

```bash
git add xtablo-expo/components/tasks/StatusControl.tsx
git commit -m "feat(expo): add StatusControl segmented picker component"
```

---

### Task 7: Create TaskRow Component

**Files:**
- Create: `xtablo-expo/components/tasks/TaskRow.tsx`

- [ ] **Step 1: Create the component**

Create `xtablo-expo/components/tasks/TaskRow.tsx`:

```typescript
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { Task, TASK_STATUSES } from "@/types/tasks.types";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Calendar } from "lucide-react-native";

type TaskRowProps = {
  task: Task;
  onPress: (task: Task) => void;
};

export default function TaskRow({ task, onPress }: TaskRowProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text");
  const borderColor = isDark ? "#374151" : "#e5e7eb";

  const statusColor = TASK_STATUSES.find((s) => s.value === task.status)?.color ?? "#9ca3af";
  const isOverdue = task.due_date && new Date(task.due_date) < new Date();

  return (
    <TouchableOpacity
      style={[styles.container, { borderBottomColor: borderColor }]}
      onPress={() => onPress(task)}
      activeOpacity={0.7}
    >
      <View style={[styles.statusDot, { backgroundColor: statusColor }]} />
      <View style={styles.content}>
        <Text style={[styles.title, { color: textColor }]} numberOfLines={1}>
          {task.title}
        </Text>
        <View style={styles.meta}>
          {task.due_date && (
            <View style={styles.dueDateBadge}>
              <Calendar size={12} color={isOverdue ? "#ef4444" : subtextColor} />
              <Text
                style={[
                  styles.dueDate,
                  { color: isOverdue ? "#ef4444" : subtextColor },
                ]}
              >
                {new Date(task.due_date).toLocaleDateString("fr-FR", {
                  day: "numeric",
                  month: "short",
                })}
              </Text>
            </View>
          )}
        </View>
      </View>
      {task.assignee_name ? (
        <View style={styles.avatar}>
          <Text style={styles.avatarText}>
            {task.assignee_name.charAt(0).toUpperCase()}
          </Text>
        </View>
      ) : (
        <View style={[styles.avatar, styles.avatarEmpty]} />
      )}
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
    gap: 12,
  },
  statusDot: {
    width: 10,
    height: 10,
    borderRadius: 5,
  },
  content: {
    flex: 1,
    gap: 2,
  },
  title: {
    fontSize: 15,
    fontWeight: "500",
  },
  meta: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  dueDateBadge: {
    flexDirection: "row",
    alignItems: "center",
    gap: 4,
  },
  dueDate: {
    fontSize: 12,
  },
  avatar: {
    width: 28,
    height: 28,
    borderRadius: 14,
    backgroundColor: "#3b82f6",
    alignItems: "center",
    justifyContent: "center",
  },
  avatarEmpty: {
    backgroundColor: "transparent",
    borderWidth: 1.5,
    borderColor: "#d1d5db",
    borderStyle: "dashed",
  },
  avatarText: {
    color: "#ffffff",
    fontSize: 13,
    fontWeight: "600",
  },
});
```

- [ ] **Step 2: Commit**

```bash
git add xtablo-expo/components/tasks/TaskRow.tsx
git commit -m "feat(expo): add TaskRow component"
```

---

### Task 8: Create EtapeSection Component

**Files:**
- Create: `xtablo-expo/components/tasks/EtapeSection.tsx`

- [ ] **Step 1: Create the component**

Create `xtablo-expo/components/tasks/EtapeSection.tsx`:

```typescript
import React, { useState } from "react";
import { View, Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
import { ChevronDown, ChevronRight, Plus, MoreHorizontal } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Etape } from "@/types/tasks.types";

type EtapeSectionProps = {
  etape: Etape | null; // null = "Sans Étape"
  taskCount: number;
  isCollapsed: boolean;
  onToggle: () => void;
  onEdit?: (etape: Etape) => void;
  onDelete?: (etape: Etape) => void;
  children: React.ReactNode;
};

export default function EtapeSection({
  etape,
  taskCount,
  isCollapsed,
  onToggle,
  onEdit,
  onDelete,
  children,
}: EtapeSectionProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const bgColor = useThemeColor({ light: "#f1f5f9", dark: "#1e293b" }, "background");
  const iconColor = isDark ? "#9ca3af" : "#6b7280";

  const title = etape ? etape.title : "Sans Étape";
  const ChevronIcon = isCollapsed ? ChevronRight : ChevronDown;

  const handleLongPress = () => {
    if (!etape) return;
    Alert.alert(etape.title, undefined, [
      { text: "Modifier", onPress: () => onEdit?.(etape) },
      {
        text: "Supprimer",
        style: "destructive",
        onPress: () => {
          Alert.alert(
            "Supprimer l'étape",
            "Les tâches associées seront déplacées dans \"Sans Étape\".",
            [
              { text: "Annuler", style: "cancel" },
              { text: "Supprimer", style: "destructive", onPress: () => onDelete?.(etape) },
            ]
          );
        },
      },
      { text: "Annuler", style: "cancel" },
    ]);
  };

  return (
    <View>
      <TouchableOpacity
        style={[styles.header, { backgroundColor: bgColor }]}
        onPress={onToggle}
        onLongPress={handleLongPress}
        activeOpacity={0.7}
      >
        <ChevronIcon size={18} color={iconColor} />
        <Text style={[styles.title, { color: textColor }]} numberOfLines={1}>
          {title}
        </Text>
        <View style={styles.badge}>
          <Text style={styles.badgeText}>{taskCount}</Text>
        </View>
      </TouchableOpacity>
      {!isCollapsed && children}
    </View>
  );
}

const styles = StyleSheet.create({
  header: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 10,
    paddingHorizontal: 16,
    gap: 8,
  },
  title: {
    flex: 1,
    fontSize: 14,
    fontWeight: "700",
  },
  badge: {
    backgroundColor: "#6b7280",
    borderRadius: 10,
    paddingHorizontal: 8,
    paddingVertical: 2,
  },
  badgeText: {
    color: "#ffffff",
    fontSize: 12,
    fontWeight: "600",
  },
});
```

- [ ] **Step 2: Commit**

```bash
git add xtablo-expo/components/tasks/EtapeSection.tsx
git commit -m "feat(expo): add EtapeSection collapsible header component"
```

---

### Task 9: Create TaskList Component

**Files:**
- Create: `xtablo-expo/components/tasks/TaskList.tsx`

- [ ] **Step 1: Create the component**

This is the main list component that combines etape sections with task rows.

Create `xtablo-expo/components/tasks/TaskList.tsx`:

```typescript
import React, { useMemo, useState } from "react";
import { View, Text, ScrollView, RefreshControl, StyleSheet, TouchableOpacity } from "react-native";
import { router } from "expo-router";
import { Plus } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useTasksByTablo } from "@/hooks/tasks";
import { useTabloEtapes } from "@/hooks/etapes";
import { useDeleteTask } from "@/hooks/tasks";
import { useDeleteEtape } from "@/hooks/etapes";
import { Task, Etape, TASK_STATUSES, TaskStatus } from "@/types/tasks.types";
import EtapeSection from "./EtapeSection";
import TaskRow from "./TaskRow";

type TaskListProps = {
  tabloId: string;
  onEditEtape: (etape: Etape) => void;
  onCreateEtape: () => void;
};

type GroupedTasks = {
  etape: Etape | null;
  tasks: Task[];
};

function groupTasksByEtape(tasks: Task[], etapes: Etape[]): GroupedTasks[] {
  const groups: GroupedTasks[] = [];

  // One section per etape, ordered by position
  for (const etape of etapes) {
    const etapeTasks = tasks.filter((t) => t.parent_task_id === etape.id);
    groups.push({ etape, tasks: etapeTasks });
  }

  // "Sans Étape" section for orphaned tasks
  const orphaned = tasks.filter((t) => !t.parent_task_id);
  if (orphaned.length > 0 || etapes.length === 0) {
    groups.push({ etape: null, tasks: orphaned });
  }

  return groups;
}

function sortTasksByStatus(tasks: Task[]): { status: TaskStatus; label: string; color: string; tasks: Task[] }[] {
  return TASK_STATUSES
    .map((s) => ({
      ...s,
      tasks: tasks.filter((t) => t.status === s.value),
    }))
    .filter((g) => g.tasks.length > 0);
}

export default function TaskList({ tabloId, onEditEtape, onCreateEtape }: TaskListProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text");

  const { data: tasks, isLoading: tasksLoading, refetch: refetchTasks } = useTasksByTablo(tabloId);
  const { data: etapes, isLoading: etapesLoading, refetch: refetchEtapes } = useTabloEtapes(tabloId);
  const { mutate: deleteTask } = useDeleteTask();
  const { mutate: deleteEtape } = useDeleteEtape();

  const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set());
  const [refreshing, setRefreshing] = useState(false);

  const groups = useMemo(
    () => groupTasksByEtape(tasks ?? [], etapes ?? []),
    [tasks, etapes]
  );

  const toggleSection = (key: string) => {
    setCollapsedSections((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  };

  const onRefresh = async () => {
    setRefreshing(true);
    await Promise.all([refetchTasks(), refetchEtapes()]);
    setRefreshing(false);
  };

  const handleTaskPress = (task: Task) => {
    router.push(`/task/${task.id}?tabloId=${tabloId}`);
  };

  const handleDeleteEtape = (etape: Etape) => {
    deleteEtape({ id: etape.id, tabloId });
  };

  const isLoading = tasksLoading || etapesLoading;

  if (isLoading && !tasks) {
    return (
      <View style={styles.centered}>
        <Text style={{ color: subtextColor }}>Chargement...</Text>
      </View>
    );
  }

  return (
    <ScrollView
      style={styles.container}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    >
      {/* Create etape button */}
      <TouchableOpacity style={styles.addEtapeButton} onPress={onCreateEtape}>
        <Plus size={16} color="#3b82f6" />
        <Text style={styles.addEtapeText}>Nouvelle étape</Text>
      </TouchableOpacity>

      {groups.map((group) => {
        const sectionKey = group.etape?.id ?? "no-etape";
        const isCollapsed = collapsedSections.has(sectionKey);
        const statusGroups = sortTasksByStatus(group.tasks);

        return (
          <EtapeSection
            key={sectionKey}
            etape={group.etape}
            taskCount={group.tasks.length}
            isCollapsed={isCollapsed}
            onToggle={() => toggleSection(sectionKey)}
            onEdit={group.etape ? onEditEtape : undefined}
            onDelete={group.etape ? handleDeleteEtape : undefined}
          >
            {statusGroups.map((sg) => (
              <View key={sg.value}>
                <View style={styles.statusHeader}>
                  <View style={[styles.statusDot, { backgroundColor: sg.color }]} />
                  <Text style={[styles.statusLabel, { color: subtextColor }]}>
                    {sg.label} ({sg.tasks.length})
                  </Text>
                </View>
                {sg.tasks.map((task) => (
                  <TaskRow key={task.id} task={task} onPress={handleTaskPress} />
                ))}
              </View>
            ))}
            {group.tasks.length === 0 && (
              <View style={styles.emptySection}>
                <Text style={{ color: subtextColor, fontSize: 13 }}>Aucune tâche</Text>
              </View>
            )}
          </EtapeSection>
        );
      })}

      {groups.length === 0 && (
        <View style={styles.emptyState}>
          <Text style={[styles.emptyTitle, { color: textColor }]}>Aucune tâche</Text>
          <Text style={{ color: subtextColor, textAlign: "center" }}>
            Créez votre première tâche avec le bouton +
          </Text>
        </View>
      )}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  centered: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    paddingTop: 40,
  },
  addEtapeButton: {
    flexDirection: "row",
    alignItems: "center",
    gap: 6,
    paddingHorizontal: 16,
    paddingVertical: 10,
  },
  addEtapeText: {
    color: "#3b82f6",
    fontSize: 14,
    fontWeight: "600",
  },
  statusHeader: {
    flexDirection: "row",
    alignItems: "center",
    gap: 6,
    paddingHorizontal: 16,
    paddingVertical: 6,
  },
  statusDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
  },
  statusLabel: {
    fontSize: 12,
    fontWeight: "600",
    textTransform: "uppercase",
  },
  emptySection: {
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  emptyState: {
    alignItems: "center",
    paddingTop: 60,
    gap: 8,
  },
  emptyTitle: {
    fontSize: 17,
    fontWeight: "600",
  },
});
```

- [ ] **Step 2: Commit**

```bash
git add xtablo-expo/components/tasks/TaskList.tsx
git commit -m "feat(expo): add TaskList component with etape grouping"
```

---

### Task 10: Create EtapeSheet Component

**Files:**
- Create: `xtablo-expo/components/tasks/EtapeSheet.tsx`

- [ ] **Step 1: Create the component**

This modal is used for creating and editing etapes.

Create `xtablo-expo/components/tasks/EtapeSheet.tsx`:

```typescript
import React, { useState, useEffect } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Modal,
  KeyboardAvoidingView,
  Platform,
  StyleSheet,
} from "react-native";
import { X } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Etape } from "@/types/tasks.types";
import { useCreateEtape, useUpdateEtape } from "@/hooks/etapes";

type EtapeSheetProps = {
  visible: boolean;
  onClose: () => void;
  tabloId: string;
  etape?: Etape | null; // null = create mode
};

export default function EtapeSheet({ visible, onClose, tabloId, etape }: EtapeSheetProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const bgColor = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background");
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const inputBg = useThemeColor({ light: "#f1f5f9", dark: "#374151" }, "background");
  const borderColor = isDark ? "#374151" : "#e5e7eb";

  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");

  const { mutate: createEtape, isPending: isCreating } = useCreateEtape();
  const { mutate: updateEtape, isPending: isUpdating } = useUpdateEtape();
  const isPending = isCreating || isUpdating;

  const isEditMode = !!etape;

  useEffect(() => {
    if (etape) {
      setTitle(etape.title);
      setDescription(etape.description ?? "");
    } else {
      setTitle("");
      setDescription("");
    }
  }, [etape, visible]);

  const handleSave = () => {
    if (!title.trim()) return;

    if (isEditMode && etape) {
      updateEtape(
        {
          id: etape.id,
          tabloId,
          title: title.trim(),
          description: description.trim() || null,
        },
        { onSuccess: onClose }
      );
    } else {
      createEtape(
        {
          tabloId,
          title: title.trim(),
          description: description.trim() || null,
        },
        { onSuccess: onClose }
      );
    }
  };

  return (
    <Modal visible={visible} animationType="slide" transparent onRequestClose={onClose}>
      <KeyboardAvoidingView
        behavior={Platform.OS === "ios" ? "padding" : "height"}
        style={styles.overlay}
      >
        <View style={[styles.sheet, { backgroundColor: bgColor }]}>
          <View style={[styles.sheetHeader, { borderBottomColor: borderColor }]}>
            <Text style={[styles.sheetTitle, { color: textColor }]}>
              {isEditMode ? "Modifier l'étape" : "Nouvelle étape"}
            </Text>
            <TouchableOpacity onPress={onClose}>
              <X size={22} color={textColor} />
            </TouchableOpacity>
          </View>

          <View style={styles.sheetBody}>
            <Text style={[styles.label, { color: textColor }]}>Titre</Text>
            <TextInput
              style={[styles.input, { backgroundColor: inputBg, color: textColor }]}
              value={title}
              onChangeText={setTitle}
              placeholder="Nom de l'étape"
              placeholderTextColor="#9ca3af"
              autoFocus
            />

            <Text style={[styles.label, { color: textColor }]}>Description</Text>
            <TextInput
              style={[styles.input, styles.textArea, { backgroundColor: inputBg, color: textColor }]}
              value={description}
              onChangeText={setDescription}
              placeholder="Description (optionnel)"
              placeholderTextColor="#9ca3af"
              multiline
              numberOfLines={3}
            />
          </View>

          <TouchableOpacity
            style={[styles.saveButton, !title.trim() && styles.saveButtonDisabled]}
            onPress={handleSave}
            disabled={!title.trim() || isPending}
          >
            <Text style={styles.saveButtonText}>
              {isPending ? "..." : isEditMode ? "Enregistrer" : "Créer"}
            </Text>
          </TouchableOpacity>
        </View>
      </KeyboardAvoidingView>
    </Modal>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    justifyContent: "flex-end",
    backgroundColor: "rgba(0,0,0,0.4)",
  },
  sheet: {
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    paddingBottom: 34,
  },
  sheetHeader: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    padding: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  sheetTitle: {
    fontSize: 17,
    fontWeight: "700",
  },
  sheetBody: {
    padding: 16,
    gap: 12,
  },
  label: {
    fontSize: 14,
    fontWeight: "600",
  },
  input: {
    borderRadius: 10,
    paddingHorizontal: 14,
    paddingVertical: 12,
    fontSize: 15,
  },
  textArea: {
    minHeight: 80,
    textAlignVertical: "top",
  },
  saveButton: {
    marginHorizontal: 16,
    backgroundColor: "#3b82f6",
    borderRadius: 12,
    paddingVertical: 14,
    alignItems: "center",
  },
  saveButtonDisabled: {
    opacity: 0.5,
  },
  saveButtonText: {
    color: "#ffffff",
    fontSize: 16,
    fontWeight: "700",
  },
});
```

- [ ] **Step 2: Commit**

```bash
git add xtablo-expo/components/tasks/EtapeSheet.tsx
git commit -m "feat(expo): add EtapeSheet modal for etape create/edit"
```

---

### Task 11: Create AssigneePicker and EtapePicker Components

**Files:**
- Create: `xtablo-expo/components/tasks/AssigneePicker.tsx`
- Create: `xtablo-expo/components/tasks/EtapePicker.tsx`

- [ ] **Step 1: Create AssigneePicker**

Create `xtablo-expo/components/tasks/AssigneePicker.tsx`:

```typescript
import React from "react";
import {
  View,
  Text,
  TouchableOpacity,
  Modal,
  FlatList,
  StyleSheet,
} from "react-native";
import { X, Check } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { TabloMember } from "@/hooks/members";

type AssigneePickerProps = {
  visible: boolean;
  onClose: () => void;
  members: TabloMember[];
  selectedId: string | null;
  onSelect: (memberId: string | null) => void;
};

export default function AssigneePicker({
  visible,
  onClose,
  members,
  selectedId,
  onSelect,
}: AssigneePickerProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const bgColor = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background");
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text");
  const borderColor = isDark ? "#374151" : "#e5e7eb";

  const handleSelect = (id: string | null) => {
    onSelect(id);
    onClose();
  };

  return (
    <Modal visible={visible} animationType="slide" transparent onRequestClose={onClose}>
      <View style={styles.overlay}>
        <View style={[styles.sheet, { backgroundColor: bgColor }]}>
          <View style={[styles.header, { borderBottomColor: borderColor }]}>
            <Text style={[styles.title, { color: textColor }]}>Assigné à</Text>
            <TouchableOpacity onPress={onClose}>
              <X size={22} color={textColor} />
            </TouchableOpacity>
          </View>

          <TouchableOpacity
            style={[styles.row, { borderBottomColor: borderColor }]}
            onPress={() => handleSelect(null)}
          >
            <View style={[styles.avatar, styles.avatarEmpty]} />
            <Text style={[styles.name, { color: subtextColor }]}>Non assigné</Text>
            {selectedId === null && <Check size={18} color="#3b82f6" />}
          </TouchableOpacity>

          <FlatList
            data={members}
            keyExtractor={(item) => item.id}
            renderItem={({ item }) => (
              <TouchableOpacity
                style={[styles.row, { borderBottomColor: borderColor }]}
                onPress={() => handleSelect(item.id)}
              >
                <View style={styles.avatar}>
                  <Text style={styles.avatarText}>
                    {item.name.charAt(0).toUpperCase()}
                  </Text>
                </View>
                <Text style={[styles.name, { color: textColor }]}>{item.name}</Text>
                {selectedId === item.id && <Check size={18} color="#3b82f6" />}
              </TouchableOpacity>
            )}
          />
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    justifyContent: "flex-end",
    backgroundColor: "rgba(0,0,0,0.4)",
  },
  sheet: {
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    maxHeight: "60%",
    paddingBottom: 34,
  },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    padding: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  title: {
    fontSize: 17,
    fontWeight: "700",
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
    gap: 12,
  },
  avatar: {
    width: 32,
    height: 32,
    borderRadius: 16,
    backgroundColor: "#3b82f6",
    alignItems: "center",
    justifyContent: "center",
  },
  avatarEmpty: {
    backgroundColor: "transparent",
    borderWidth: 1.5,
    borderColor: "#d1d5db",
    borderStyle: "dashed",
  },
  avatarText: {
    color: "#ffffff",
    fontSize: 14,
    fontWeight: "600",
  },
  name: {
    flex: 1,
    fontSize: 15,
  },
});
```

- [ ] **Step 2: Create EtapePicker**

Create `xtablo-expo/components/tasks/EtapePicker.tsx`:

```typescript
import React from "react";
import {
  View,
  Text,
  TouchableOpacity,
  Modal,
  FlatList,
  StyleSheet,
} from "react-native";
import { X, Check } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Etape } from "@/types/tasks.types";

type EtapePickerProps = {
  visible: boolean;
  onClose: () => void;
  etapes: Etape[];
  selectedId: string | null;
  onSelect: (etapeId: string | null) => void;
};

export default function EtapePicker({
  visible,
  onClose,
  etapes,
  selectedId,
  onSelect,
}: EtapePickerProps) {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const bgColor = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background");
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text");
  const borderColor = isDark ? "#374151" : "#e5e7eb";

  const handleSelect = (id: string | null) => {
    onSelect(id);
    onClose();
  };

  return (
    <Modal visible={visible} animationType="slide" transparent onRequestClose={onClose}>
      <View style={styles.overlay}>
        <View style={[styles.sheet, { backgroundColor: bgColor }]}>
          <View style={[styles.header, { borderBottomColor: borderColor }]}>
            <Text style={[styles.title, { color: textColor }]}>Étape</Text>
            <TouchableOpacity onPress={onClose}>
              <X size={22} color={textColor} />
            </TouchableOpacity>
          </View>

          <TouchableOpacity
            style={[styles.row, { borderBottomColor: borderColor }]}
            onPress={() => handleSelect(null)}
          >
            <Text style={[styles.name, { color: subtextColor }]}>Sans Étape</Text>
            {selectedId === null && <Check size={18} color="#3b82f6" />}
          </TouchableOpacity>

          <FlatList
            data={etapes}
            keyExtractor={(item) => item.id}
            renderItem={({ item }) => (
              <TouchableOpacity
                style={[styles.row, { borderBottomColor: borderColor }]}
                onPress={() => handleSelect(item.id)}
              >
                <Text style={[styles.name, { color: textColor }]}>{item.title}</Text>
                {selectedId === item.id && <Check size={18} color="#3b82f6" />}
              </TouchableOpacity>
            )}
          />
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    justifyContent: "flex-end",
    backgroundColor: "rgba(0,0,0,0.4)",
  },
  sheet: {
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    maxHeight: "50%",
    paddingBottom: 34,
  },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    padding: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  title: {
    fontSize: 17,
    fontWeight: "700",
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    paddingVertical: 14,
    paddingHorizontal: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  name: {
    flex: 1,
    fontSize: 15,
  },
});
```

- [ ] **Step 3: Commit**

```bash
git add xtablo-expo/components/tasks/AssigneePicker.tsx xtablo-expo/components/tasks/EtapePicker.tsx
git commit -m "feat(expo): add AssigneePicker and EtapePicker modal components"
```

---

### Task 12: Create Tablo Detail Screen

**Files:**
- Create: `xtablo-expo/app/(app)/tablo/_layout.tsx`
- Create: `xtablo-expo/app/(app)/tablo/[id].tsx`
- Modify: `xtablo-expo/app/(app)/_layout.tsx` — Register tablo Stack.Screen

- [ ] **Step 1: Create tablo layout**

Create `xtablo-expo/app/(app)/tablo/_layout.tsx`:

```typescript
import { Stack } from "expo-router";

export default function TabloLayout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}
```

- [ ] **Step 2: Create tablo detail screen**

Create `xtablo-expo/app/(app)/tablo/[id].tsx`:

```typescript
import React, { useState } from "react";
import { View, Text, TouchableOpacity, StyleSheet, SafeAreaView } from "react-native";
import { useLocalSearchParams, router } from "expo-router";
import { ArrowLeft, Plus } from "lucide-react-native";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useTablosList } from "@/hooks/tablos";
import { Etape } from "@/types/tasks.types";
import { ColorMap } from "@/constants/colors";
import TaskList from "@/components/tasks/TaskList";
import EtapeSheet from "@/components/tasks/EtapeSheet";

export default function TabloDetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const bgColor = useThemeColor({ light: "#f8fafc", dark: "#111827" }, "background");
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const headerBg = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background");
  const borderColor = isDark ? "#374151" : "#e5e7eb";

  const { data: tablos } = useTablosList();
  const tablo = tablos?.find((t) => t.id === id);

  const [etapeSheetVisible, setEtapeSheetVisible] = useState(false);
  const [editingEtape, setEditingEtape] = useState<Etape | null>(null);

  const handleEditEtape = (etape: Etape) => {
    setEditingEtape(etape);
    setEtapeSheetVisible(true);
  };

  const handleCreateEtape = () => {
    setEditingEtape(null);
    setEtapeSheetVisible(true);
  };

  const handleCloseEtapeSheet = () => {
    setEtapeSheetVisible(false);
    setEditingEtape(null);
  };

  if (!tablo || !id) {
    return (
      <SafeAreaView style={[styles.container, { backgroundColor: bgColor }]}>
        <Text style={{ color: textColor }}>Tablo introuvable</Text>
      </SafeAreaView>
    );
  }

  const tabloColor = tablo.color ? ColorMap[tablo.color] ?? "#3b82f6" : "#3b82f6";

  return (
    <SafeAreaView style={[styles.container, { backgroundColor: bgColor }]}>
      {/* Header */}
      <View style={[styles.header, { backgroundColor: headerBg, borderBottomColor: borderColor }]}>
        <TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
          <ArrowLeft size={22} color={textColor} />
        </TouchableOpacity>
        <View style={[styles.colorIndicator, { backgroundColor: tabloColor }]} />
        <Text style={[styles.tabloName, { color: textColor }]} numberOfLines={1}>
          {tablo.name}
        </Text>
      </View>

      {/* Task list */}
      <TaskList
        tabloId={id}
        onEditEtape={handleEditEtape}
        onCreateEtape={handleCreateEtape}
      />

      {/* FAB - create task */}
      <TouchableOpacity
        style={styles.fab}
        onPress={() => router.push(`/task/new?tabloId=${id}`)}
      >
        <Plus size={24} color="#ffffff" />
      </TouchableOpacity>

      {/* Etape sheet */}
      <EtapeSheet
        visible={etapeSheetVisible}
        onClose={handleCloseEtapeSheet}
        tabloId={id}
        etape={editingEtape}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    paddingHorizontal: 12,
    paddingVertical: 14,
    borderBottomWidth: StyleSheet.hairlineWidth,
    gap: 10,
  },
  backButton: {
    padding: 4,
  },
  colorIndicator: {
    width: 14,
    height: 14,
    borderRadius: 4,
  },
  tabloName: {
    flex: 1,
    fontSize: 18,
    fontWeight: "700",
  },
  fab: {
    position: "absolute",
    bottom: 24,
    right: 20,
    width: 54,
    height: 54,
    borderRadius: 27,
    backgroundColor: "#3b82f6",
    alignItems: "center",
    justifyContent: "center",
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.2,
    shadowRadius: 6,
    elevation: 6,
  },
});
```

- [ ] **Step 3: Register tablo route in app layout**

Open `xtablo-expo/app/(app)/_layout.tsx` and add the tablo Stack.Screen. Change from:

```typescript
<Stack>
  <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
  <Stack.Screen name="channel" options={{ headerShown: false }} />
</Stack>
```

To:

```typescript
<Stack>
  <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
  <Stack.Screen name="channel" options={{ headerShown: false }} />
  <Stack.Screen name="tablo" options={{ headerShown: false }} />
  <Stack.Screen name="task" options={{ headerShown: false }} />
</Stack>
```

- [ ] **Step 4: Verify it compiles**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | head -20`
Expected: No new errors.

- [ ] **Step 5: Commit**

```bash
git add xtablo-expo/app/(app)/tablo/ xtablo-expo/app/(app)/_layout.tsx
git commit -m "feat(expo): add tablo detail screen with task list and etape management"
```

---

### Task 13: Create Task Detail Screen

**Files:**
- Create: `xtablo-expo/app/(app)/task/_layout.tsx`
- Create: `xtablo-expo/app/(app)/task/[id].tsx`

- [ ] **Step 1: Create task layout**

Create `xtablo-expo/app/(app)/task/_layout.tsx`:

```typescript
import { Stack } from "expo-router";

export default function TaskLayout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}
```

- [ ] **Step 2: Create task detail screen**

Create `xtablo-expo/app/(app)/task/[id].tsx`:

```typescript
import React, { useState, useEffect } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  ScrollView,
  StyleSheet,
  SafeAreaView,
  Alert,
  Platform,
} from "react-native";
import { useLocalSearchParams, router } from "expo-router";
import { ArrowLeft, User, Layers, Calendar } from "lucide-react-native";
import DateTimePicker from "@react-native-community/datetimepicker";
import { useThemeColor } from "@/hooks/useThemeColor";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useTasksByTablo, useCreateTask, useUpdateTask, useDeleteTask } from "@/hooks/tasks";
import { useTabloEtapes } from "@/hooks/etapes";
import { useTabloMembers } from "@/hooks/members";
import { TaskStatus, TASK_STATUSES } from "@/types/tasks.types";
import StatusControl from "@/components/tasks/StatusControl";
import AssigneePicker from "@/components/tasks/AssigneePicker";
import EtapePicker from "@/components/tasks/EtapePicker";

export default function TaskDetailScreen() {
  const { id, tabloId } = useLocalSearchParams<{ id: string; tabloId: string }>();
  const isCreateMode = id === "new";

  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const bgColor = useThemeColor({ light: "#f8fafc", dark: "#111827" }, "background");
  const textColor = useThemeColor({ light: "#1f2937", dark: "#f9fafb" }, "text");
  const headerBg = useThemeColor({ light: "#ffffff", dark: "#1f2937" }, "background");
  const inputBg = useThemeColor({ light: "#f1f5f9", dark: "#374151" }, "background");
  const borderColor = isDark ? "#374151" : "#e5e7eb";
  const subtextColor = useThemeColor({ light: "#6b7280", dark: "#9ca3af" }, "text");

  // Data
  const { data: tasks } = useTasksByTablo(tabloId);
  const { data: etapes } = useTabloEtapes(tabloId);
  const { data: members } = useTabloMembers(tabloId);
  const existingTask = isCreateMode ? null : tasks?.find((t) => t.id === id);

  // Form state
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [status, setStatus] = useState<TaskStatus>("todo");
  const [assigneeId, setAssigneeId] = useState<string | null>(null);
  const [parentTaskId, setParentTaskId] = useState<string | null>(null);
  const [dueDate, setDueDate] = useState<Date | null>(null);
  const [showDatePicker, setShowDatePicker] = useState(false);

  // Picker visibility
  const [assigneePickerVisible, setAssigneePickerVisible] = useState(false);
  const [etapePickerVisible, setEtapePickerVisible] = useState(false);

  // Mutations
  const { mutate: createTask, isPending: isCreating } = useCreateTask();
  const { mutate: updateTask, isPending: isUpdating } = useUpdateTask();
  const { mutate: deleteTask, isPending: isDeleting } = useDeleteTask();
  const isPending = isCreating || isUpdating || isDeleting;

  // Populate form for edit mode
  useEffect(() => {
    if (existingTask) {
      setTitle(existingTask.title);
      setDescription(existingTask.description ?? "");
      setStatus(existingTask.status);
      setAssigneeId(existingTask.assignee_id);
      setParentTaskId(existingTask.parent_task_id);
      setDueDate(existingTask.due_date ? new Date(existingTask.due_date) : null);
    }
  }, [existingTask]);

  const handleSave = () => {
    if (!title.trim() || !tabloId) return;

    const dueDateStr = dueDate
      ? `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, "0")}-${String(dueDate.getDate()).padStart(2, "0")}`
      : null;

    if (isCreateMode) {
      createTask(
        {
          tablo_id: tabloId,
          title: title.trim(),
          description: description.trim() || null,
          status,
          assignee_id: assigneeId,
          parent_task_id: parentTaskId,
          due_date: dueDateStr,
        },
        { onSuccess: () => router.back() }
      );
    } else {
      updateTask(
        {
          id: id!,
          tabloId: tabloId!,
          title: title.trim(),
          description: description.trim() || null,
          status,
          assignee_id: assigneeId,
          parent_task_id: parentTaskId,
          due_date: dueDateStr,
        },
        { onSuccess: () => router.back() }
      );
    }
  };

  const handleDelete = () => {
    Alert.alert("Supprimer la tâche", "Cette action est irréversible.", [
      { text: "Annuler", style: "cancel" },
      {
        text: "Supprimer",
        style: "destructive",
        onPress: () => {
          deleteTask({ id: id!, tabloId: tabloId! }, { onSuccess: () => router.back() });
        },
      },
    ]);
  };

  // Derived display values
  const assigneeName = members?.find((m) => m.id === assigneeId)?.name ?? "Non assigné";
  const etapeName = etapes?.find((e) => e.id === parentTaskId)?.title ?? "Sans Étape";
  const dueDateDisplay = dueDate
    ? dueDate.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" })
    : "Aucune";

  return (
    <SafeAreaView style={[styles.container, { backgroundColor: bgColor }]}>
      {/* Header */}
      <View style={[styles.header, { backgroundColor: headerBg, borderBottomColor: borderColor }]}>
        <TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
          <ArrowLeft size={22} color={textColor} />
        </TouchableOpacity>
        <Text style={[styles.headerTitle, { color: textColor }]}>
          {isCreateMode ? "Nouvelle tâche" : "Modifier la tâche"}
        </Text>
      </View>

      <ScrollView style={styles.form} keyboardShouldPersistTaps="handled">
        {/* Title */}
        <Text style={[styles.label, { color: textColor }]}>Titre</Text>
        <TextInput
          style={[styles.input, { backgroundColor: inputBg, color: textColor }]}
          value={title}
          onChangeText={setTitle}
          placeholder="Titre de la tâche"
          placeholderTextColor="#9ca3af"
          autoFocus={isCreateMode}
        />

        {/* Description */}
        <Text style={[styles.label, { color: textColor }]}>Description</Text>
        <TextInput
          style={[styles.input, styles.textArea, { backgroundColor: inputBg, color: textColor }]}
          value={description}
          onChangeText={setDescription}
          placeholder="Description (optionnel)"
          placeholderTextColor="#9ca3af"
          multiline
          numberOfLines={4}
        />

        {/* Status */}
        <Text style={[styles.label, { color: textColor }]}>Statut</Text>
        <StatusControl value={status} onChange={setStatus} />

        {/* Assignee */}
        <Text style={[styles.label, { color: textColor }]}>Assigné à</Text>
        <TouchableOpacity
          style={[styles.pickerButton, { backgroundColor: inputBg }]}
          onPress={() => setAssigneePickerVisible(true)}
        >
          <User size={16} color={subtextColor} />
          <Text style={[styles.pickerText, { color: assigneeId ? textColor : subtextColor }]}>
            {assigneeName}
          </Text>
        </TouchableOpacity>

        {/* Etape */}
        <Text style={[styles.label, { color: textColor }]}>Étape</Text>
        <TouchableOpacity
          style={[styles.pickerButton, { backgroundColor: inputBg }]}
          onPress={() => setEtapePickerVisible(true)}
        >
          <Layers size={16} color={subtextColor} />
          <Text style={[styles.pickerText, { color: parentTaskId ? textColor : subtextColor }]}>
            {etapeName}
          </Text>
        </TouchableOpacity>

        {/* Due date */}
        <Text style={[styles.label, { color: textColor }]}>Date limite</Text>
        <TouchableOpacity
          style={[styles.pickerButton, { backgroundColor: inputBg }]}
          onPress={() => setShowDatePicker(true)}
        >
          <Calendar size={16} color={subtextColor} />
          <Text style={[styles.pickerText, { color: dueDate ? textColor : subtextColor }]}>
            {dueDateDisplay}
          </Text>
          {dueDate && (
            <TouchableOpacity onPress={() => setDueDate(null)}>
              <Text style={{ color: "#ef4444", fontSize: 13 }}>Retirer</Text>
            </TouchableOpacity>
          )}
        </TouchableOpacity>

        {showDatePicker && (
          <DateTimePicker
            value={dueDate ?? new Date()}
            mode="date"
            display={Platform.OS === "ios" ? "spinner" : "default"}
            onChange={(event, date) => {
              setShowDatePicker(Platform.OS !== "ios");
              if (date) setDueDate(date);
            }}
          />
        )}

        {/* Save button */}
        <TouchableOpacity
          style={[styles.saveButton, !title.trim() && styles.saveButtonDisabled]}
          onPress={handleSave}
          disabled={!title.trim() || isPending}
        >
          <Text style={styles.saveButtonText}>
            {isPending ? "..." : isCreateMode ? "Créer" : "Enregistrer"}
          </Text>
        </TouchableOpacity>

        {/* Delete button (edit mode only) */}
        {!isCreateMode && (
          <TouchableOpacity style={styles.deleteButton} onPress={handleDelete} disabled={isPending}>
            <Text style={styles.deleteButtonText}>Supprimer la tâche</Text>
          </TouchableOpacity>
        )}

        <View style={{ height: 40 }} />
      </ScrollView>

      {/* Pickers */}
      <AssigneePicker
        visible={assigneePickerVisible}
        onClose={() => setAssigneePickerVisible(false)}
        members={members ?? []}
        selectedId={assigneeId}
        onSelect={setAssigneeId}
      />
      <EtapePicker
        visible={etapePickerVisible}
        onClose={() => setEtapePickerVisible(false)}
        etapes={etapes ?? []}
        selectedId={parentTaskId}
        onSelect={setParentTaskId}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    paddingHorizontal: 12,
    paddingVertical: 14,
    borderBottomWidth: StyleSheet.hairlineWidth,
    gap: 10,
  },
  backButton: {
    padding: 4,
  },
  headerTitle: {
    fontSize: 18,
    fontWeight: "700",
  },
  form: {
    flex: 1,
    padding: 16,
  },
  label: {
    fontSize: 14,
    fontWeight: "600",
    marginTop: 16,
    marginBottom: 6,
  },
  input: {
    borderRadius: 10,
    paddingHorizontal: 14,
    paddingVertical: 12,
    fontSize: 15,
  },
  textArea: {
    minHeight: 100,
    textAlignVertical: "top",
  },
  pickerButton: {
    flexDirection: "row",
    alignItems: "center",
    borderRadius: 10,
    paddingHorizontal: 14,
    paddingVertical: 12,
    gap: 10,
  },
  pickerText: {
    flex: 1,
    fontSize: 15,
  },
  saveButton: {
    backgroundColor: "#3b82f6",
    borderRadius: 12,
    paddingVertical: 14,
    alignItems: "center",
    marginTop: 24,
  },
  saveButtonDisabled: {
    opacity: 0.5,
  },
  saveButtonText: {
    color: "#ffffff",
    fontSize: 16,
    fontWeight: "700",
  },
  deleteButton: {
    borderRadius: 12,
    paddingVertical: 14,
    alignItems: "center",
    marginTop: 12,
  },
  deleteButtonText: {
    color: "#ef4444",
    fontSize: 15,
    fontWeight: "600",
  },
});
```

- [ ] **Step 3: Verify it compiles**

Run: `cd xtablo-expo && npx tsc --noEmit --pretty 2>&1 | head -20`
Expected: No new errors. Note: `@react-native-community/datetimepicker` may need to be installed — see Task 14.

- [ ] **Step 4: Commit**

```bash
git add xtablo-expo/app/(app)/task/
git commit -m "feat(expo): add task detail screen with create/edit/delete"
```

---

### Task 14: Install DateTimePicker Dependency

**Files:**
- Modify: `xtablo-expo/package.json`

The task detail screen uses `@react-native-community/datetimepicker` for native date picking. Check if it's already installed.

- [ ] **Step 1: Check if already installed**

Run: `cd xtablo-expo && grep datetimepicker package.json`
Expected: Either a match (already installed) or no output (needs install).

- [ ] **Step 2: Install if missing**

If not installed, run:
```bash
cd xtablo-expo && npx expo install @react-native-community/datetimepicker
```

- [ ] **Step 3: Commit if changed**

```bash
git add xtablo-expo/package.json xtablo-expo/yarn.lock xtablo-expo/package-lock.json 2>/dev/null
git commit -m "chore(expo): add @react-native-community/datetimepicker dependency"
```

---

### Task 15: Update Tablos List Navigation

**Files:**
- Modify: `xtablo-expo/app/(app)/(tabs)/tablos.tsx`

Currently tapping a tablo navigates to the chat channel. Change it to navigate to the new tablo detail screen.

- [ ] **Step 1: Find the navigation handler**

Open `xtablo-expo/app/(app)/(tabs)/tablos.tsx`. Find the `navigateToTablo` function or the `onPress` handler for tablo items. It currently does something like:

```typescript
router.push(`/channel/${getChannelCid(tablo.id)}`);
```

- [ ] **Step 2: Change navigation target**

Replace the tablo tap handler to navigate to the tablo detail screen instead:

```typescript
const navigateToTablo = (tablo: UserTablo) => {
  router.push(`/tablo/${tablo.id}`);
};
```

Keep any existing "Chat" quick action button that navigates to the channel — just change the main tablo tap target.

- [ ] **Step 3: Verify the app runs**

Run: `cd xtablo-expo && npx expo start`
Open on simulator/device. Tap a tablo — should navigate to the new tablo detail screen.

- [ ] **Step 4: Commit**

```bash
git add xtablo-expo/app/(app)/(tabs)/tablos.tsx
git commit -m "feat(expo): navigate to tablo detail on tap instead of channel"
```

---

### Task 16: End-to-End Smoke Test

No new files. This is a manual verification task.

- [ ] **Step 1: Start the app**

Run: `cd xtablo-expo && npx expo start`

- [ ] **Step 2: Test tablo detail**

- Open the app
- Tap a tablo → should see tablo detail screen with header and task list
- Verify back button returns to tablos list

- [ ] **Step 3: Test task creation**

- Tap the "+" FAB on tablo detail
- Fill in title, set status, pick assignee/etape/due date
- Tap "Créer"
- Verify the task appears in the task list under the correct etape section

- [ ] **Step 4: Test task editing**

- Tap an existing task
- Change title, status, assignee
- Tap "Enregistrer"
- Verify changes are reflected in the list

- [ ] **Step 5: Test task deletion**

- Tap a task to edit
- Tap "Supprimer la tâche"
- Confirm deletion
- Verify task is removed from the list

- [ ] **Step 6: Test etape management**

- Tap "Nouvelle étape" on the task list
- Create an etape with title
- Verify it appears as a section header
- Long-press the etape header → Edit → change title → save
- Long-press the etape header → Delete → confirm
- Verify child tasks move to "Sans Étape"

- [ ] **Step 7: Test pull-to-refresh**

- Pull down on the task list
- Verify data refreshes

- [ ] **Step 8: Test collapse/expand**

- Tap an etape section header
- Verify tasks collapse
- Tap again → verify they expand
