-
- Aucune Étape n'a encore été définie pour ce tablo.
-
+ {t("tablo:etape.noEtapes")}
{canManageEtapes ? (
- Créez votre première Étape pour structurer les tâches du tablo.
+ {t("tablo:etape.createFirstEtape")}
) : (
- Seul le propriétaire du tablo peut ajouter des Étapes.
+ {t("tablo:etape.onlyOwnerCanAdd")}
)}
- Vue d'ensemble
+
+ {t("tablo:overview.title")}
+
- Configurez les Étapes du tablo pour clarifier les grandes phases de votre projet.
+ {t("tablo:overview.description")}
{!canManageEtapes && (
- Seul le propriétaire du tablo peut modifier les Étapes. Contactez l'administrateur si vous
- avez besoin d'une nouvelle Étape.
+ {t("tablo:etape.onlyOwnerCanModify")}
)}
- {/* Status Pill with Etape Color */}
+ {/* Status Pill */}
{etapeTitle ?? "Sans Étape"}
diff --git a/apps/main/src/hooks/tablos.ts b/apps/main/src/hooks/tablos.ts
index 845f60d..207a3c6 100644
--- a/apps/main/src/hooks/tablos.ts
+++ b/apps/main/src/hooks/tablos.ts
@@ -29,16 +29,18 @@ export const useTablosList = () => {
// Fetch single tablo
export const useTablo = (id: string) => {
+ const user = useUser();
return useQuery({
queryKey: ["tablos", id],
queryFn: async () => {
const { data, error } = await supabase
- .from("tablos")
+ .from("user_tablos")
.select("*")
.eq("id", id)
- .is("deleted_at", null);
+ .eq("user_id", user.id)
+ .single();
if (error) throw error;
- return data[0];
+ return data as UserTablo;
},
});
};
diff --git a/apps/main/src/i18n.test.ts b/apps/main/src/i18n.test.ts
index 9e8ba5e..5254707 100644
--- a/apps/main/src/i18n.test.ts
+++ b/apps/main/src/i18n.test.ts
@@ -10,9 +10,34 @@ import notesEn from "./locales/en/notes.json";
import pagesEn from "./locales/en/pages.json";
import planningEn from "./locales/en/planning.json";
import settingsEn from "./locales/en/settings.json";
+import tabloEn from "./locales/en/tablo.json";
+import authFr from "./locales/fr/auth.json";
+import availabilitiesFr from "./locales/fr/availabilities.json";
+import commonFr from "./locales/fr/common.json";
+import componentsFr from "./locales/fr/components.json";
+import modalsFr from "./locales/fr/modals.json";
+import navigationFr from "./locales/fr/navigation.json";
+import notesFr from "./locales/fr/notes.json";
+import pagesFr from "./locales/fr/pages.json";
+import planningFr from "./locales/fr/planning.json";
+import settingsFr from "./locales/fr/settings.json";
+import tabloFr from "./locales/fr/tablo.json";
i18n.use(initReactI18next).init({
resources: {
+ fr: {
+ common: commonFr,
+ navigation: navigationFr,
+ pages: pagesFr,
+ settings: settingsFr,
+ availabilities: availabilitiesFr,
+ auth: authFr,
+ planning: planningFr,
+ modals: modalsFr,
+ components: componentsFr,
+ notes: notesFr,
+ tablo: tabloFr,
+ },
en: {
common: commonEn,
navigation: navigationEn,
@@ -24,6 +49,7 @@ i18n.use(initReactI18next).init({
modals: modalsEn,
components: componentsEn,
notes: notesEn,
+ tablo: tabloEn,
},
},
lng: "en",
diff --git a/apps/main/src/i18n.ts b/apps/main/src/i18n.ts
index 8651a92..a9007bc 100644
--- a/apps/main/src/i18n.ts
+++ b/apps/main/src/i18n.ts
@@ -11,6 +11,7 @@ import notesEn from "./locales/en/notes.json";
import pagesEn from "./locales/en/pages.json";
import planningEn from "./locales/en/planning.json";
import settingsEn from "./locales/en/settings.json";
+import tabloEn from "./locales/en/tablo.json";
import authFr from "./locales/fr/auth.json";
import availabilitiesFr from "./locales/fr/availabilities.json";
// Import translation files
@@ -22,6 +23,7 @@ import notesFr from "./locales/fr/notes.json";
import pagesFr from "./locales/fr/pages.json";
import planningFr from "./locales/fr/planning.json";
import settingsFr from "./locales/fr/settings.json";
+import tabloFr from "./locales/fr/tablo.json";
i18n
.use(LanguageDetector)
@@ -39,6 +41,7 @@ i18n
modals: modalsFr,
components: componentsFr,
notes: notesFr,
+ tablo: tabloFr,
},
en: {
common: commonEn,
@@ -51,6 +54,7 @@ i18n
modals: modalsEn,
components: componentsEn,
notes: notesEn,
+ tablo: tabloEn,
},
},
lng: "fr",
diff --git a/apps/main/src/locales/en/common.json b/apps/main/src/locales/en/common.json
index df19b99..1063175 100644
--- a/apps/main/src/locales/en/common.json
+++ b/apps/main/src/locales/en/common.json
@@ -27,7 +27,10 @@
"sort": "Sort",
"loading": "Loading...",
"saving": "Saving...",
- "deleting": "Deleting..."
+ "deleting": "Deleting...",
+ "save": "Save",
+ "cancel": "Cancel",
+ "add": "Add"
},
"messages": {
"success": "Success",
@@ -36,6 +39,9 @@
"info": "Information",
"confirm_delete": "Are you sure you want to delete this item?"
},
+ "errors": {
+ "error": "Error"
+ },
"labels": {
"name": "Name",
"description": "Description",
diff --git a/apps/main/src/locales/en/modals.json b/apps/main/src/locales/en/modals.json
index 1c80012..5d60675 100644
--- a/apps/main/src/locales/en/modals.json
+++ b/apps/main/src/locales/en/modals.json
@@ -1,7 +1,7 @@
{
"createTablo": {
- "title": "Create a new project",
- "nameLabel": "Project name",
- "namePlaceholder": "Enter project name"
+ "title": "Create a new tablo",
+ "nameLabel": "Tablo name",
+ "namePlaceholder": "Enter tablo name"
}
}
diff --git a/apps/main/src/locales/en/navigation.json b/apps/main/src/locales/en/navigation.json
index c9841bc..9126912 100644
--- a/apps/main/src/locales/en/navigation.json
+++ b/apps/main/src/locales/en/navigation.json
@@ -1,5 +1,5 @@
{
- "projects": "Projects",
+ "projects": "Tablos",
"myEvents": "My Events",
"planning": "Planning",
"discussions": "Discussions",
diff --git a/apps/main/src/locales/en/pages.json b/apps/main/src/locales/en/pages.json
index fc73ecb..69c40f1 100644
--- a/apps/main/src/locales/en/pages.json
+++ b/apps/main/src/locales/en/pages.json
@@ -1,12 +1,12 @@
{
"tablo": {
- "title": "Projects",
- "subtitle": "Manage your projects and collaborations",
- "createButton": "New project",
+ "title": "Tablos",
+ "subtitle": "Manage your tablos and collaborations",
+ "createButton": "New tablo",
"emptyState": {
- "title": "No projects found",
- "description": "Create your first project to start organizing your work",
- "button": "Create your first project"
+ "title": "No tablos found",
+ "description": "Create your first tablo to start organizing your work",
+ "button": "Create your first tablo"
},
"filter": {
"all": "All",
@@ -30,7 +30,7 @@
"contextMenu": {
"openDiscussions": "Open discussions",
"openPlanning": "Open planning",
- "delete": "Delete project"
+ "delete": "Delete tablo"
},
"kpis": {
"total": "Total",
diff --git a/apps/main/src/locales/en/planning.json b/apps/main/src/locales/en/planning.json
index 4a62118..e662292 100644
--- a/apps/main/src/locales/en/planning.json
+++ b/apps/main/src/locales/en/planning.json
@@ -1,8 +1,8 @@
{
"title": "Planning",
"allEvents": "All events",
- "allTablos": "All projects",
- "selectTablo": "Select a project",
+ "allTablos": "All tablos",
+ "selectTablo": "Select a tablo",
"createEvent": "Create event",
"importPlanning": "Import planning",
"today": "Today",
diff --git a/apps/main/src/locales/en/tablo.json b/apps/main/src/locales/en/tablo.json
new file mode 100644
index 0000000..57b522a
--- /dev/null
+++ b/apps/main/src/locales/en/tablo.json
@@ -0,0 +1,26 @@
+{
+ "overview": {
+ "title": "Overview",
+ "description": "Configure the Stages of the tablo to clarify the major phases of your tablo.",
+ "overallProgress": "Overall Progress",
+ "progressSummary": "{{done}} of {{total}} task(s) completed"
+ },
+ "etape": {
+ "nameRequired": "The Stage name is required",
+ "namePlaceholder": "Stage Name",
+ "deleteConfirm": "Are you sure you want to delete the Stage \"{{name}}\"? Associated tasks will remain available.",
+ "noEtapes": "No Stages have been defined for this tablo yet.",
+ "createFirstEtape": "Create your first Stage to structure the tablo tasks.",
+ "onlyOwnerCanAdd": "Only the tablo owner can add Stages.",
+ "onlyOwnerCanModify": "Only the tablo owner can modify Stages. Contact the administrator if you need a new Stage.",
+ "stepNumber": "Stage {{number}}",
+ "addNew": "New Stage"
+ },
+ "tasks": {
+ "task_singular": "task",
+ "task_plural": "tasks",
+ "inProgress": "in progress",
+ "completed_singular": "completed",
+ "completed_plural": "completed"
+ }
+}
diff --git a/apps/main/src/locales/fr/common.json b/apps/main/src/locales/fr/common.json
index 6d816e2..d31dfb8 100644
--- a/apps/main/src/locales/fr/common.json
+++ b/apps/main/src/locales/fr/common.json
@@ -27,7 +27,10 @@
"sort": "Trier",
"loading": "Chargement...",
"saving": "Enregistrement...",
- "deleting": "Suppression..."
+ "deleting": "Suppression...",
+ "save": "Enregistrer",
+ "cancel": "Annuler",
+ "add": "Ajouter"
},
"messages": {
"success": "Succès",
@@ -36,6 +39,9 @@
"info": "Information",
"confirm_delete": "Êtes-vous sûr de vouloir supprimer cet élément ?"
},
+ "errors": {
+ "error": "Erreur"
+ },
"labels": {
"name": "Nom",
"description": "Description",
diff --git a/apps/main/src/locales/fr/modals.json b/apps/main/src/locales/fr/modals.json
index 06bf548..854baaa 100644
--- a/apps/main/src/locales/fr/modals.json
+++ b/apps/main/src/locales/fr/modals.json
@@ -1,7 +1,7 @@
{
"createTablo": {
- "title": "Créer un nouveau projet",
- "nameLabel": "Nom du projet",
- "namePlaceholder": "Entrez le nom du projet"
+ "title": "Créer un nouveau tablo",
+ "nameLabel": "Nom du tablo",
+ "namePlaceholder": "Entrez le nom du tablo"
}
}
diff --git a/apps/main/src/locales/fr/navigation.json b/apps/main/src/locales/fr/navigation.json
index c63ab6d..6e4899d 100644
--- a/apps/main/src/locales/fr/navigation.json
+++ b/apps/main/src/locales/fr/navigation.json
@@ -1,5 +1,5 @@
{
- "projects": "Projets",
+ "projects": "Tablos",
"myEvents": "Mes Événements",
"planning": "Planning",
"discussions": "Discussions",
diff --git a/apps/main/src/locales/fr/pages.json b/apps/main/src/locales/fr/pages.json
index 2fe04a9..57fbd97 100644
--- a/apps/main/src/locales/fr/pages.json
+++ b/apps/main/src/locales/fr/pages.json
@@ -1,16 +1,16 @@
{
"tablo": {
- "title": "Projets",
- "subtitle": "Gérez vos projets et collaborations",
- "createButton": "Nouveau projet",
+ "title": "Tablos",
+ "subtitle": "Gérez vos tablos et collaborations",
+ "createButton": "Nouveau tablo",
"emptyState": {
- "title": "Aucun projet trouvé",
- "description": "Créez votre premier projet pour commencer à organiser votre travail",
- "button": "Créer votre premier projet"
+ "title": "Aucun tablo trouvé",
+ "description": "Créez votre premier tablo pour commencer à organiser votre travail",
+ "button": "Créer votre premier tablo"
},
"filter": {
"all": "Tous",
- "todo": "À faire",
+ "todo": "Pas commencé",
"inProgress": "En cours",
"done": "Terminé"
},
@@ -30,7 +30,7 @@
"contextMenu": {
"openDiscussions": "Ouvrir les discussions",
"openPlanning": "Ouvrir le planning",
- "delete": "Supprimer le projet"
+ "delete": "Supprimer le tablo"
},
"kpis": {
"total": "Total",
diff --git a/apps/main/src/locales/fr/planning.json b/apps/main/src/locales/fr/planning.json
index 5a67a92..65a592e 100644
--- a/apps/main/src/locales/fr/planning.json
+++ b/apps/main/src/locales/fr/planning.json
@@ -1,8 +1,8 @@
{
"title": "Planning",
"allEvents": "Tous les événements",
- "allTablos": "Tous les projets",
- "selectTablo": "Sélectionner un projet",
+ "allTablos": "Tous les tablos",
+ "selectTablo": "Sélectionner un tablo",
"createEvent": "Créer un événement",
"importPlanning": "Importer un planning",
"today": "Aujourd'hui",
diff --git a/apps/main/src/locales/fr/tablo.json b/apps/main/src/locales/fr/tablo.json
new file mode 100644
index 0000000..c9f0187
--- /dev/null
+++ b/apps/main/src/locales/fr/tablo.json
@@ -0,0 +1,26 @@
+{
+ "overview": {
+ "title": "Vue d'ensemble",
+ "description": "Configurez les Étapes du tablo pour clarifier les grandes phases de votre tablo.",
+ "overallProgress": "Progression globale",
+ "progressSummary": "{{done}} sur {{total}} tâche(s) terminée(s)"
+ },
+ "etape": {
+ "nameRequired": "Le nom de l'Étape est requis",
+ "namePlaceholder": "Nom de l'Étape",
+ "deleteConfirm": "Êtes-vous sûr de vouloir supprimer l'Étape \"{{name}}\" ? Les tâches associées resteront disponibles.",
+ "noEtapes": "Aucune Étape n'a encore été définie pour ce tablo.",
+ "createFirstEtape": "Créez votre première Étape pour structurer les tâches du tablo.",
+ "onlyOwnerCanAdd": "Seul le propriétaire du tablo peut ajouter des Étapes.",
+ "onlyOwnerCanModify": "Seul le propriétaire du tablo peut modifier les Étapes. Contactez l'administrateur si vous avez besoin d'une nouvelle Étape.",
+ "stepNumber": "Étape {{number}}",
+ "addNew": "Ajouter l'Étape"
+ },
+ "tasks": {
+ "task_singular": "tâche",
+ "task_plural": "tâches",
+ "inProgress": "en cours",
+ "completed_singular": "terminée",
+ "completed_plural": "terminées"
+ }
+}
diff --git a/apps/main/src/pages/PublicBookingPage.test.tsx b/apps/main/src/pages/PublicBookingPage.test.tsx
index b8fa8bc..cf01146 100644
--- a/apps/main/src/pages/PublicBookingPage.test.tsx
+++ b/apps/main/src/pages/PublicBookingPage.test.tsx
@@ -2,12 +2,6 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { PublicBookingPage } from "./PublicBookingPage";
-vi.mock("react-i18next", () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- }),
-}));
-
vi.mock("../hooks/eventTypes", () => ({
usePublicEventType: () => ({
eventType: {
diff --git a/apps/main/src/pages/PublicNotePage.test.tsx b/apps/main/src/pages/PublicNotePage.test.tsx
index d8d8b93..c9c5446 100644
--- a/apps/main/src/pages/PublicNotePage.test.tsx
+++ b/apps/main/src/pages/PublicNotePage.test.tsx
@@ -3,12 +3,6 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { PublicNotePage } from "./PublicNotePage";
-vi.mock("react-i18next", () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- }),
-}));
-
vi.mock("../hooks/notes", () => ({
usePublicNote: () => ({
note: {
diff --git a/apps/main/src/pages/feedback.test.tsx b/apps/main/src/pages/feedback.test.tsx
index 9f1f091..21196a4 100644
--- a/apps/main/src/pages/feedback.test.tsx
+++ b/apps/main/src/pages/feedback.test.tsx
@@ -4,12 +4,6 @@ import { FeedbackPage } from "./feedback";
const mockCreateFeedback = vi.fn();
-vi.mock("react-i18next", () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- }),
-}));
-
vi.mock("../hooks/feedback", () => ({
useCreateFeedback: () => ({
createFeedback: mockCreateFeedback,
diff --git a/apps/main/src/pages/landing.tsx b/apps/main/src/pages/landing.tsx
index ceb0cc5..3594a4b 100644
--- a/apps/main/src/pages/landing.tsx
+++ b/apps/main/src/pages/landing.tsx
@@ -40,7 +40,7 @@ export const LandingPage = () => {
- Un client, un projet, un espace de travail
+ Un client, un tablo, un espace de travail
Avec XTablo, créez un groupe et discutez avec vos clients et collaborateurs sans email,
@@ -92,7 +92,7 @@ export const LandingPage = () => {
🤔 Le problème aujourd'hui
- Vous lancez un projet avec un client ?
+ Vous lancez un tablo avec un client ?
Vous jonglez entre messages WhatsApp, briefs Notion, liens Calendly, et retours par
e-mail ?
@@ -164,7 +164,7 @@ export function PrivacyPolicyPage() {
Fourniture du service : Créer et
gérer votre compte, vous permettre d'accéder à nos fonctionnalités de
- planification, de collaboration et de gestion de projets.
+ planification, de collaboration et de gestion de tablos.
Communication : Vous envoyer des
@@ -226,8 +226,8 @@ export function PrivacyPolicyPage() {
Membres de votre équipe : Les
- utilisateurs avec lesquels vous collaborez sur des projets partagés peuvent
- accéder aux informations que vous partagez volontairement.
+ utilisateurs avec lesquels vous collaborez sur des tablos partagés peuvent accéder
+ aux informations que vous partagez volontairement.
diff --git a/apps/main/src/utils/etapeColors.ts b/apps/main/src/utils/etapeColors.ts
index 822f506..42a82bc 100644
--- a/apps/main/src/utils/etapeColors.ts
+++ b/apps/main/src/utils/etapeColors.ts
@@ -6,35 +6,33 @@ export const getEtapeColor = (position: number) => {
const colors = [
{
// Light/Yellow
- bg: "bg-yellow-100 dark:bg-yellow-950/30",
- text: "text-yellow-700 dark:text-yellow-400",
- border: "border-yellow-200 dark:border-yellow-900/50",
+ bg: "bg-yellow-50 dark:bg-yellow-950/20",
+ text: "text-yellow-600 dark:text-yellow-300",
+ border: "border-yellow-100 dark:border-yellow-900/30",
indicator: "bg-yellow-400 dark:bg-yellow-500",
},
{
// Green
- bg: "bg-green-100 dark:bg-green-950/30",
- text: "text-green-700 dark:text-green-400",
- border: "border-green-200 dark:border-green-900/50",
+ bg: "bg-green-50 dark:bg-green-950/20",
+ text: "text-green-600 dark:text-green-300",
+ border: "border-green-100 dark:border-green-900/30",
indicator: "bg-green-400 dark:bg-green-500",
},
{
// Blue
- bg: "bg-blue-100 dark:bg-blue-950/30",
- text: "text-blue-700 dark:text-blue-400",
- border: "border-blue-200 dark:border-blue-900/50",
+ bg: "bg-blue-50 dark:bg-blue-950/20",
+ text: "text-blue-600 dark:text-blue-300",
+ border: "border-blue-100 dark:border-blue-900/30",
indicator: "bg-blue-400 dark:bg-blue-500",
},
{
// Gray
- bg: "bg-gray-100 dark:bg-gray-800/30",
- text: "text-gray-700 dark:text-gray-400",
- border: "border-gray-200 dark:border-gray-700/50",
+ bg: "bg-gray-50 dark:bg-gray-800/20",
+ text: "text-gray-600 dark:text-gray-300",
+ border: "border-gray-100 dark:border-gray-700/30",
indicator: "bg-gray-400 dark:bg-gray-500",
},
];
return colors[position % colors.length];
};
-
-
diff --git a/apps/main/src/utils/testHelpers.tsx b/apps/main/src/utils/testHelpers.tsx
index 6146cf5..b093879 100644
--- a/apps/main/src/utils/testHelpers.tsx
+++ b/apps/main/src/utils/testHelpers.tsx
@@ -4,7 +4,9 @@ import userEvent from "@testing-library/user-event";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
import React from "react";
+import { I18nextProvider } from "react-i18next";
import { BrowserRouter, MemoryRouter, Route, Routes } from "react-router-dom";
+import testI18n from "../i18n.test";
import { TestUserStoreProvider } from "../providers/UserStoreProvider";
const defaultUser = {
@@ -33,12 +35,15 @@ export const renderWithRouter = (ui: React.ReactNode, { route = "/" } = {}) => {
interface RenderWithProvidersOptions {
route?: string;
path?: string;
+ language?: string;
}
export const renderWithProviders = (
ui: React.ReactNode,
- { route, path }: RenderWithProvidersOptions = {}
+ { route, path, language = "en" }: RenderWithProvidersOptions = {}
): RenderResult => {
+ // Set the language for this test
+ testI18n.changeLanguage(language);
// Create a new QueryClient instance for each test to avoid state pollution
const testQueryClient = new QueryClient({
defaultOptions: {
@@ -62,28 +67,30 @@ export const renderWithProviders = (
);
return render(
-
-
-
-
- {content}
-
-
-
-
+
+
+
+
+
+ {content}
+
+
+
+
+
);
};
diff --git a/packages/shared-types/src/database.types.ts b/packages/shared-types/src/database.types.ts
index 7a53e32..abcd4e3 100644
--- a/packages/shared-types/src/database.types.ts
+++ b/packages/shared-types/src/database.types.ts
@@ -1,10 +1,30 @@
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
export type Database = {
- // Allows to automatically instantiate createClient with right options
- // instead of createClient(URL, KEY)
- __InternalSupabase: {
- PostgrestVersion: "13.0.4";
+ graphql_public: {
+ Tables: {
+ [_ in never]: never;
+ };
+ Views: {
+ [_ in never]: never;
+ };
+ Functions: {
+ graphql: {
+ Args: {
+ extensions?: Json;
+ operationName?: string;
+ query?: string;
+ variables?: Json;
+ };
+ Returns: Json;
+ };
+ };
+ Enums: {
+ [_ in never]: never;
+ };
+ CompositeTypes: {
+ [_ in never]: never;
+ };
};
public: {
Tables: {
@@ -570,7 +590,7 @@ export type Database = {
name: string;
owner_id: string;
position: number;
- status: string;
+ status: string | null;
updated_at: string | null;
};
Insert: {
@@ -582,7 +602,7 @@ export type Database = {
name: string;
owner_id: string;
position?: number;
- status?: string;
+ status?: string | null;
updated_at?: string | null;
};
Update: {
@@ -594,7 +614,7 @@ export type Database = {
name?: string;
owner_id?: string;
position?: number;
- status?: string;
+ status?: string | null;
updated_at?: string | null;
};
Relationships: [];
@@ -795,6 +815,10 @@ export type Database = {
};
};
Functions: {
+ compute_tablo_status: {
+ Args: { tablo_id_param: string };
+ Returns: string;
+ };
generate_random_string: { Args: { length?: number }; Returns: string };
get_my_active_subscription: {
Args: never;
@@ -1017,6 +1041,9 @@ export type CompositeTypes<
: never;
export const Constants = {
+ graphql_public: {
+ Enums: {},
+ },
public: {
Enums: {
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
diff --git a/supabase/migrations/20251122000000_deprecate_tablo_status.sql b/supabase/migrations/20251122000000_deprecate_tablo_status.sql
new file mode 100644
index 0000000..3e63817
--- /dev/null
+++ b/supabase/migrations/20251122000000_deprecate_tablo_status.sql
@@ -0,0 +1,129 @@
+-- Migration: Deprecate tablo status and compute it from etapes
+-- This migration creates a function to compute tablo status from etapes,
+-- updates the user_tablos view to use the computed status, and makes
+-- the status column nullable for backwards compatibility.
+
+-- Create function to compute tablo status from etapes (parent tasks)
+CREATE OR REPLACE FUNCTION "public"."compute_tablo_status"("tablo_id_param" "text")
+RETURNS "text"
+LANGUAGE "plpgsql"
+STABLE
+AS $$
+DECLARE
+ etape_count INTEGER;
+ total_tasks INTEGER;
+ done_tasks INTEGER;
+ in_progress_tasks INTEGER;
+ computed_status TEXT;
+BEGIN
+ -- Count total etapes for this tablo
+ SELECT COUNT(*)
+ INTO etape_count
+ FROM "public"."tasks"
+ WHERE "tablo_id" = tablo_id_param
+ AND "is_parent" = true;
+
+ -- If no etapes exist, return 'todo'
+ IF etape_count = 0 THEN
+ RETURN 'todo';
+ END IF;
+
+ -- Count tasks across all etapes (excluding parent tasks)
+ SELECT
+ COUNT(*),
+ COUNT(CASE WHEN "status" = 'done' THEN 1 END),
+ COUNT(CASE WHEN "status" IN ('in_progress', 'in_review') THEN 1 END)
+ INTO total_tasks, done_tasks, in_progress_tasks
+ FROM "public"."tasks"
+ WHERE "tablo_id" = tablo_id_param
+ AND "is_parent" = false;
+
+ -- If no child tasks exist, consider all etapes as done (empty etapes)
+ IF total_tasks = 0 THEN
+ RETURN 'done';
+ END IF;
+
+ -- Determine status based on task counts
+ -- Priority order: done > in_progress > todo
+ IF done_tasks = total_tasks THEN
+ -- All tasks are done
+ computed_status := 'done';
+ ELSIF in_progress_tasks > 0 THEN
+ -- At least one task is actively in progress or in review
+ computed_status := 'in_progress';
+ ELSIF done_tasks > 0 THEN
+ -- Some tasks are done but none are in progress (showing progress)
+ computed_status := 'in_progress';
+ ELSE
+ -- All tasks are todo (no progress has been made)
+ computed_status := 'todo';
+ END IF;
+
+ RETURN computed_status;
+END;
+$$;
+
+ALTER FUNCTION "public"."compute_tablo_status"("text") OWNER TO "postgres";
+
+COMMENT ON FUNCTION "public"."compute_tablo_status"("text") IS 'Computes the status of a tablo based on its etapes (parent tasks). Returns todo, in_progress, or done.';
+
+-- Update the user_tablos view to use computed status
+CREATE OR REPLACE VIEW "public"."user_tablos" WITH ("security_invoker"='true') AS
+SELECT DISTINCT
+ "t"."id",
+ "ta"."user_id",
+ "t"."name",
+ "t"."image",
+ "t"."color",
+ CAST("public"."compute_tablo_status"("t"."id") AS character varying(20)) AS "status",
+ "t"."position",
+ "t"."created_at",
+ "t"."deleted_at",
+ CASE
+ WHEN ("ta"."is_admin" = true) THEN 'admin'::"text"
+ ELSE 'member'::"text"
+ END AS "access_level",
+ "ta"."is_admin"
+FROM ("public"."tablos" "t"
+ LEFT JOIN "public"."tablo_access" "ta" ON (("t"."id" = "ta"."tablo_id")))
+WHERE (("ta"."is_active" = true) AND ("t"."deleted_at" IS NULL))
+ORDER BY "t"."position", "t"."created_at" DESC;
+
+ALTER TABLE "public"."user_tablos" OWNER TO "postgres";
+
+COMMENT ON VIEW "public"."user_tablos" IS 'View that returns all tablos accessible to the current authenticated user, with status computed from etapes';
+
+-- Make the status column nullable in tablos table (for backwards compatibility)
+-- Remove the NOT NULL constraint
+ALTER TABLE "public"."tablos"
+ ALTER COLUMN "status" DROP NOT NULL;
+
+-- Remove the CHECK constraint on status
+ALTER TABLE "public"."tablos"
+ DROP CONSTRAINT IF EXISTS "tablos_status_check";
+
+-- Add a comment to indicate the column is deprecated
+COMMENT ON COLUMN "public"."tablos"."status" IS 'DEPRECATED: Status is now computed from etapes. This column is kept for backwards compatibility but should not be used.';
+
+-- Update events_and_tablos view to use computed status as well
+CREATE OR REPLACE VIEW "public"."events_and_tablos" WITH ("security_invoker"='true') AS
+SELECT DISTINCT
+ "e"."id" AS "event_id",
+ "e"."title",
+ "e"."start_date",
+ "e"."start_time",
+ "e"."end_time",
+ "e"."description",
+ "t"."id" AS "tablo_id",
+ "t"."name" AS "tablo_name",
+ "t"."color" AS "tablo_color",
+ CAST("public"."compute_tablo_status"("t"."id") AS character varying(20)) AS "tablo_status"
+FROM "public"."events" "e"
+LEFT JOIN "public"."tablos" "t" ON ("e"."tablo_id" = "t"."id")
+WHERE ("e"."deleted_at" IS NULL) AND ("t"."deleted_at" IS NULL)
+ORDER BY "e"."start_date", "e"."start_time";
+
+ALTER TABLE "public"."events_and_tablos" OWNER TO "postgres";
+
+COMMENT ON VIEW "public"."events_and_tablos" IS 'View combining events with their associated tablo information, with status computed from etapes';
+
diff --git a/supabase/migrations/20251122000001_fix_tests_after_status_deprecation.sql b/supabase/migrations/20251122000001_fix_tests_after_status_deprecation.sql
new file mode 100644
index 0000000..85c7f22
--- /dev/null
+++ b/supabase/migrations/20251122000001_fix_tests_after_status_deprecation.sql
@@ -0,0 +1,167 @@
+-- Fix test failures after deprecating tablo status
+-- This migration addresses:
+-- 1. Re-add check constraint on status to allow NULL or valid values
+-- 2. Fix RLS policies for etape creation/update to properly block non-owners
+
+-- ============================================================================
+-- Fix 1: Add back status check constraint (but allow NULL)
+-- ============================================================================
+
+-- Add check constraint that allows NULL or the valid status values
+ALTER TABLE "public"."tablos"
+ ADD CONSTRAINT "tablos_status_check"
+ CHECK (
+ "status" IS NULL
+ OR ("status")::"text" = ANY (ARRAY[
+ ('todo'::character varying)::"text",
+ ('in_progress'::character varying)::"text",
+ ('done'::character varying)::"text"
+ ])
+ );
+
+COMMENT ON CONSTRAINT "tablos_status_check" ON "public"."tablos"
+ IS 'Allows NULL or valid status values (todo, in_progress, done). Status is deprecated and computed from etapes.';
+
+-- ============================================================================
+-- Fix 2: Ensure RLS policies properly prevent non-owners from managing etapes
+-- ============================================================================
+
+-- The issue is that the RLS policies need to explicitly check that the user
+-- is the tablo owner when dealing with etapes. Let's verify the policies
+-- are correctly preventing non-owner access to parent tasks.
+
+-- Drop and recreate the INSERT policy with more explicit checks
+DROP POLICY IF EXISTS "Users can create tasks in their tablos" ON "public"."tasks";
+
+CREATE POLICY "Users can create tasks in their tablos"
+ ON "public"."tasks"
+ FOR INSERT
+ WITH CHECK (
+ -- User must have access to the tablo
+ EXISTS (
+ SELECT 1 FROM "public"."tablo_access" "ta"
+ WHERE "ta"."tablo_id" = "tasks"."tablo_id"
+ AND "ta"."user_id" = auth.uid()
+ AND "ta"."is_active" = true
+ -- If creating an etape (is_parent = true), user must be admin
+ AND (
+ (NOT "tasks"."is_parent")
+ OR "ta"."is_admin" = true
+ )
+ )
+ );
+
+-- Drop and recreate the UPDATE policy with more explicit checks
+DROP POLICY IF EXISTS "Users can update tasks in their tablos" ON "public"."tasks";
+
+CREATE POLICY "Users can update tasks in their tablos"
+ ON "public"."tasks"
+ FOR UPDATE
+ USING (
+ -- User must have access to the tablo
+ EXISTS (
+ SELECT 1 FROM "public"."tablo_access" "ta"
+ WHERE "ta"."tablo_id" = "tasks"."tablo_id"
+ AND "ta"."user_id" = auth.uid()
+ AND "ta"."is_active" = true
+ -- If updating an etape (is_parent = true), user must be admin
+ AND (
+ (NOT "tasks"."is_parent")
+ OR "ta"."is_admin" = true
+ )
+ )
+ )
+ WITH CHECK (
+ -- Same check for the new values
+ EXISTS (
+ SELECT 1 FROM "public"."tablo_access" "ta"
+ WHERE "ta"."tablo_id" = "tasks"."tablo_id"
+ AND "ta"."user_id" = auth.uid()
+ AND "ta"."is_active" = true
+ -- If the result would be an etape, user must be admin
+ AND (
+ (NOT "tasks"."is_parent")
+ OR "ta"."is_admin" = true
+ )
+ )
+ );
+
+-- Drop and recreate the DELETE policy for consistency
+DROP POLICY IF EXISTS "Users can delete tasks in their tablos" ON "public"."tasks";
+
+CREATE POLICY "Users can delete tasks in their tablos"
+ ON "public"."tasks"
+ FOR DELETE
+ USING (
+ -- User must have access to the tablo
+ EXISTS (
+ SELECT 1 FROM "public"."tablo_access" "ta"
+ WHERE "ta"."tablo_id" = "tasks"."tablo_id"
+ AND "ta"."user_id" = auth.uid()
+ AND "ta"."is_active" = true
+ -- If deleting an etape (is_parent = true), user must be admin
+ AND (
+ (NOT "tasks"."is_parent")
+ OR "ta"."is_admin" = true
+ )
+ )
+ );
+
+COMMENT ON POLICY "Users can create tasks in their tablos" ON "public"."tasks"
+ IS 'Users can create regular tasks in tablos they have access to. Only tablo admins can create etapes (is_parent=true).';
+
+COMMENT ON POLICY "Users can update tasks in their tablos" ON "public"."tasks"
+ IS 'Users can update regular tasks in tablos they have access to. Only tablo admins can update etapes (is_parent=true).';
+
+COMMENT ON POLICY "Users can delete tasks in their tablos" ON "public"."tasks"
+ IS 'Users can delete regular tasks in tablos they have access to. Only tablo admins can delete etapes (is_parent=true).';
+
+-- ============================================================================
+-- Fix 3: Add trigger to explicitly validate etape permissions and raise error
+-- ============================================================================
+
+-- Create function that validates etape permissions before INSERT/UPDATE
+CREATE OR REPLACE FUNCTION "public"."validate_etape_permissions"()
+RETURNS TRIGGER
+LANGUAGE "plpgsql"
+SECURITY DEFINER
+AS $$
+DECLARE
+ user_is_admin boolean;
+BEGIN
+ -- Only validate if this is an etape (is_parent = true)
+ IF NEW.is_parent = true THEN
+ -- Check if the current user is an admin of this tablo
+ SELECT "ta"."is_admin" INTO user_is_admin
+ FROM "public"."tablo_access" "ta"
+ WHERE "ta"."tablo_id" = NEW.tablo_id
+ AND "ta"."user_id" = auth.uid()
+ AND "ta"."is_active" = true
+ LIMIT 1;
+
+ -- If user is not found or not an admin, raise an error with proper SQLSTATE
+ IF user_is_admin IS NULL OR user_is_admin = false THEN
+ RAISE EXCEPTION USING
+ ERRCODE = '42501', -- insufficient_privilege
+ MESSAGE = 'Only tablo admins can create or modify etapes',
+ HINT = 'Contact the tablo owner to request admin access';
+ END IF;
+ END IF;
+
+ RETURN NEW;
+END;
+$$;
+
+ALTER FUNCTION "public"."validate_etape_permissions"() OWNER TO "postgres";
+
+COMMENT ON FUNCTION "public"."validate_etape_permissions"()
+ IS 'Validates that only tablo admins can create or update etapes (tasks with is_parent=true)';
+
+-- Create trigger that runs before INSERT/UPDATE on tasks
+DROP TRIGGER IF EXISTS "validate_etape_permissions_trigger" ON "public"."tasks";
+
+CREATE TRIGGER "validate_etape_permissions_trigger"
+ BEFORE INSERT OR UPDATE ON "public"."tasks"
+ FOR EACH ROW
+ EXECUTE FUNCTION "public"."validate_etape_permissions"();
+
diff --git a/supabase/tests/database/01_schema_structure.test.sql b/supabase/tests/database/01_schema_structure.test.sql
index bee7539..a5cf916 100644
--- a/supabase/tests/database/01_schema_structure.test.sql
+++ b/supabase/tests/database/01_schema_structure.test.sql
@@ -50,7 +50,8 @@ SELECT col_type_is('public', 'tablos', 'position', 'integer', 'tablos.position s
SELECT col_not_null('public', 'tablos', 'owner_id', 'tablos.owner_id should be NOT NULL');
SELECT col_not_null('public', 'tablos', 'name', 'tablos.name should be NOT NULL');
-SELECT col_not_null('public', 'tablos', 'status', 'tablos.status should be NOT NULL');
+-- Note: status is now nullable and computed from etapes
+SELECT col_is_null('public', 'tablos', 'status', 'tablos.status is now nullable (deprecated)');
SELECT col_has_default('public', 'tablos', 'status', 'tablos.status should have default');
SELECT col_has_default('public', 'tablos', 'position', 'tablos.position should have default');
diff --git a/supabase/tests/database/12_compute_tablo_status.test.sql b/supabase/tests/database/12_compute_tablo_status.test.sql
new file mode 100644
index 0000000..5a85827
--- /dev/null
+++ b/supabase/tests/database/12_compute_tablo_status.test.sql
@@ -0,0 +1,354 @@
+BEGIN;
+SELECT plan(15);
+
+-- ============================================================================
+-- Setup Test Data
+-- ============================================================================
+
+DO $$
+DECLARE
+ test_user_id uuid := gen_random_uuid();
+ tablo_no_etapes text;
+ tablo_all_done text;
+ tablo_in_progress text;
+ tablo_mixed text;
+ tablo_empty_etapes text;
+ etape1_id text;
+ etape2_id text;
+ etape3_id text;
+ etape4_id text;
+ etape5_id text;
+BEGIN
+ -- Insert test user
+ INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at)
+ VALUES
+ (test_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'status_test_' || test_user_id::text || '@test.com', 'encrypted', now(), now(), now())
+ ON CONFLICT DO NOTHING;
+
+ -- Insert test profile
+ INSERT INTO public.profiles (id, email, first_name, last_name, short_user_id)
+ VALUES
+ (test_user_id, 'status_test_' || test_user_id::text || '@test.com', 'Status', 'Test', substring(test_user_id::text from 1 for 8))
+ ON CONFLICT DO NOTHING;
+
+ -- Set JWT context
+ PERFORM set_config('request.jwt.claims', json_build_object('sub', test_user_id::text)::text, true);
+
+ -- Create test tablos
+ -- Tablo 1: No etapes
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (test_user_id, 'Tablo No Etapes', 0)
+ RETURNING id INTO tablo_no_etapes;
+
+ -- Tablo 2: All etapes done
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (test_user_id, 'Tablo All Done', 1)
+ RETURNING id INTO tablo_all_done;
+
+ -- Tablo 3: Has in_progress tasks
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (test_user_id, 'Tablo In Progress', 2)
+ RETURNING id INTO tablo_in_progress;
+
+ -- Tablo 4: Mixed statuses
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (test_user_id, 'Tablo Mixed', 3)
+ RETURNING id INTO tablo_mixed;
+
+ -- Tablo 5: Etapes without child tasks
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (test_user_id, 'Tablo Empty Etapes', 4)
+ RETURNING id INTO tablo_empty_etapes;
+
+ -- Setup Tablo 2: All etapes done
+ -- Etape 1 with all tasks done
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (tablo_all_done, 'Etape 1', 'done', 0, true)
+ RETURNING id INTO etape1_id;
+
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (tablo_all_done, 'Task 1.1', 'done', 0, false, etape1_id),
+ (tablo_all_done, 'Task 1.2', 'done', 1, false, etape1_id);
+
+ -- Etape 2 with all tasks done
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (tablo_all_done, 'Etape 2', 'done', 1, true)
+ RETURNING id INTO etape2_id;
+
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (tablo_all_done, 'Task 2.1', 'done', 0, false, etape2_id);
+
+ -- Setup Tablo 3: Has in_progress tasks
+ -- Etape 1 with in_progress task
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (tablo_in_progress, 'Etape 1', 'in_progress', 0, true)
+ RETURNING id INTO etape3_id;
+
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (tablo_in_progress, 'Task 1.1', 'in_progress', 0, false, etape3_id),
+ (tablo_in_progress, 'Task 1.2', 'done', 1, false, etape3_id);
+
+ -- Setup Tablo 4: Mixed statuses
+ -- Etape 1: all done
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (tablo_mixed, 'Etape 1', 'done', 0, true)
+ RETURNING id INTO etape4_id;
+
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (tablo_mixed, 'Task 1.1', 'done', 0, false, etape4_id);
+
+ -- Etape 2: has todo tasks (not started)
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (tablo_mixed, 'Etape 2', 'todo', 1, true)
+ RETURNING id INTO etape5_id;
+
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (tablo_mixed, 'Task 2.1', 'todo', 0, false, etape5_id);
+
+ -- Setup Tablo 5: Empty etapes (no child tasks)
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES
+ (tablo_empty_etapes, 'Empty Etape 1', 'todo', 0, true),
+ (tablo_empty_etapes, 'Empty Etape 2', 'todo', 1, true);
+
+ -- Store test IDs
+ PERFORM set_config('test.user_id', test_user_id::text, true);
+ PERFORM set_config('test.tablo_no_etapes', tablo_no_etapes, true);
+ PERFORM set_config('test.tablo_all_done', tablo_all_done, true);
+ PERFORM set_config('test.tablo_in_progress', tablo_in_progress, true);
+ PERFORM set_config('test.tablo_mixed', tablo_mixed, true);
+ PERFORM set_config('test.tablo_empty_etapes', tablo_empty_etapes, true);
+END $$;
+
+-- ============================================================================
+-- Test 1: Function exists and is accessible
+-- ============================================================================
+
+SELECT has_function(
+ 'public',
+ 'compute_tablo_status',
+ ARRAY['text'],
+ 'compute_tablo_status function should exist'
+);
+
+SELECT function_returns(
+ 'public',
+ 'compute_tablo_status',
+ ARRAY['text'],
+ 'text',
+ 'compute_tablo_status should return text'
+);
+
+-- ============================================================================
+-- Test 2: Tablo with no etapes should return 'todo'
+-- ============================================================================
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.tablo_no_etapes')),
+ 'todo',
+ 'Tablo with no etapes should have status "todo"'
+);
+
+-- ============================================================================
+-- Test 3: Tablo with all etapes done should return 'done'
+-- ============================================================================
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.tablo_all_done')),
+ 'done',
+ 'Tablo with all etapes completed should have status "done"'
+);
+
+-- ============================================================================
+-- Test 4: Tablo with in_progress tasks should return 'in_progress'
+-- ============================================================================
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.tablo_in_progress')),
+ 'in_progress',
+ 'Tablo with tasks in progress should have status "in_progress"'
+);
+
+-- ============================================================================
+-- Test 5: Tablo with mixed etapes (some done, some todo) should return 'in_progress'
+-- ============================================================================
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.tablo_mixed')),
+ 'in_progress',
+ 'Tablo with mixed etapes (some done, some todo) should have status "in_progress"'
+);
+
+-- ============================================================================
+-- Test 6: Tablo with empty etapes (no child tasks) should return 'done'
+-- ============================================================================
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.tablo_empty_etapes')),
+ 'done',
+ 'Tablo with etapes that have no child tasks should have status "done"'
+);
+
+-- ============================================================================
+-- Test 6.5: Tablo with partial progress (some tasks done, some todo) should return 'in_progress'
+-- ============================================================================
+
+DO $$
+DECLARE
+ partial_tablo_id text;
+ partial_etape_id text;
+BEGIN
+ -- Create a tablo with partial progress
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (current_setting('test.user_id')::uuid, 'Partial Progress Tablo', 7)
+ RETURNING id INTO partial_tablo_id;
+
+ -- Create an etape
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (partial_tablo_id, 'Partial Etape', 'in_progress', 0, true)
+ RETURNING id INTO partial_etape_id;
+
+ -- Create multiple tasks with mixed statuses
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES
+ (partial_tablo_id, 'Done Task 1', 'done', 0, false, partial_etape_id),
+ (partial_tablo_id, 'Done Task 2', 'done', 1, false, partial_etape_id),
+ (partial_tablo_id, 'Todo Task 1', 'todo', 2, false, partial_etape_id),
+ (partial_tablo_id, 'Todo Task 2', 'todo', 3, false, partial_etape_id);
+
+ PERFORM set_config('test.partial_tablo', partial_tablo_id, true);
+END $$;
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.partial_tablo')),
+ 'in_progress',
+ 'Tablo with some tasks done and some todo should have status "in_progress"'
+);
+
+-- ============================================================================
+-- Test 7: Verify status computation in user_tablos view
+-- ============================================================================
+
+SELECT is(
+ (SELECT status FROM public.user_tablos WHERE id = current_setting('test.tablo_no_etapes')),
+ 'todo',
+ 'user_tablos view should show computed status for tablo with no etapes'
+);
+
+SELECT is(
+ (SELECT status FROM public.user_tablos WHERE id = current_setting('test.tablo_all_done')),
+ 'done',
+ 'user_tablos view should show computed status for tablo with all done'
+);
+
+SELECT is(
+ (SELECT status FROM public.user_tablos WHERE id = current_setting('test.tablo_in_progress')),
+ 'in_progress',
+ 'user_tablos view should show computed status for tablo in progress'
+);
+
+-- ============================================================================
+-- Test 8: Test status changes when tasks are updated
+-- ============================================================================
+
+DO $$
+DECLARE
+ test_tablo_id text;
+ test_etape_id text;
+ test_task_id text;
+BEGIN
+ -- Create a new tablo for dynamic testing
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (current_setting('test.user_id')::uuid, 'Dynamic Test Tablo', 5)
+ RETURNING id INTO test_tablo_id;
+
+ -- Create an etape
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (test_tablo_id, 'Dynamic Etape', 'todo', 0, true)
+ RETURNING id INTO test_etape_id;
+
+ -- Create a todo task
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES (test_tablo_id, 'Dynamic Task', 'todo', 0, false, test_etape_id)
+ RETURNING id INTO test_task_id;
+
+ -- Store IDs for testing
+ PERFORM set_config('test.dynamic_tablo', test_tablo_id, true);
+ PERFORM set_config('test.dynamic_task', test_task_id, true);
+END $$;
+
+-- Initially should be 'todo'
+SELECT is(
+ public.compute_tablo_status(current_setting('test.dynamic_tablo')),
+ 'todo',
+ 'New tablo with todo task should have status "todo"'
+);
+
+-- Update task to in_progress
+DO $$
+BEGIN
+ UPDATE public.tasks
+ SET status = 'in_progress'
+ WHERE id = current_setting('test.dynamic_task');
+END $$;
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.dynamic_tablo')),
+ 'in_progress',
+ 'Tablo should change to "in_progress" when task is updated'
+);
+
+-- Update task to done
+DO $$
+BEGIN
+ UPDATE public.tasks
+ SET status = 'done'
+ WHERE id = current_setting('test.dynamic_task');
+END $$;
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.dynamic_tablo')),
+ 'done',
+ 'Tablo should change to "done" when all tasks are completed'
+);
+
+-- ============================================================================
+-- Test 9: Test with tasks in 'in_review' status (should count as in_progress)
+-- ============================================================================
+
+DO $$
+DECLARE
+ review_tablo_id text;
+ review_etape_id text;
+BEGIN
+ -- Create a tablo with in_review tasks
+ INSERT INTO public.tablos (owner_id, name, position)
+ VALUES (current_setting('test.user_id')::uuid, 'Review Test Tablo', 6)
+ RETURNING id INTO review_tablo_id;
+
+ -- Create an etape
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent)
+ VALUES (review_tablo_id, 'Review Etape', 'in_review', 0, true)
+ RETURNING id INTO review_etape_id;
+
+ -- Create an in_review task
+ INSERT INTO public.tasks (tablo_id, title, status, position, is_parent, parent_task_id)
+ VALUES (review_tablo_id, 'Review Task', 'in_review', 0, false, review_etape_id);
+
+ PERFORM set_config('test.review_tablo', review_tablo_id, true);
+END $$;
+
+SELECT is(
+ public.compute_tablo_status(current_setting('test.review_tablo')),
+ 'in_progress',
+ 'Tablo with in_review tasks should have status "in_progress"'
+);
+
+SELECT * FROM finish();
+ROLLBACK;
+
diff --git a/xtablo-expo/lib/database.types.ts b/xtablo-expo/lib/database.types.ts
index c352661..ff9fc4c 100644
--- a/xtablo-expo/lib/database.types.ts
+++ b/xtablo-expo/lib/database.types.ts
@@ -576,7 +576,7 @@ export type Database = {
name: string
owner_id: string
position: number
- status: string
+ status: string | null
updated_at: string | null
}
Insert: {
@@ -588,7 +588,7 @@ export type Database = {
name: string
owner_id: string
position?: number
- status?: string
+ status?: string | null
updated_at?: string | null
}
Update: {
@@ -600,7 +600,7 @@ export type Database = {
name?: string
owner_id?: string
position?: number
- status?: string
+ status?: string | null
updated_at?: string | null
}
Relationships: []
@@ -801,6 +801,10 @@ export type Database = {
}
}
Functions: {
+ compute_tablo_status: {
+ Args: { tablo_id_param: string }
+ Returns: string
+ }
generate_random_string: { Args: { length?: number }; Returns: string }
get_my_active_subscription: {
Args: never