Add CRUD tablos

This commit is contained in:
Arthur Belleville 2025-06-30 22:34:04 +02:00
parent 976b51ca1b
commit 5366524602
No known key found for this signature in database
7 changed files with 685 additions and 262 deletions

View file

@ -0,0 +1,30 @@
-- Create tablos table
CREATE TABLE IF NOT EXISTS tablos (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
image TEXT,
color VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'todo',
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
-- Constraint to ensure status is one of the allowed values
CONSTRAINT tablos_status_check CHECK (status IN ('todo', 'in_progress', 'done'))
);
-- Enable Row Level Security
ALTER TABLE tablos ENABLE ROW LEVEL SECURITY;
-- Create policy to allow users to see only their own tablos
CREATE POLICY "Users can view own tablos" ON tablos
FOR SELECT USING (auth.uid() = user_id);
-- Create policy to allow users to insert their own tablos
CREATE POLICY "Users can insert own tablos" ON tablos
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Create policy to allow users to update their own tablos
CREATE POLICY "Users can update own tablos" ON tablos
FOR UPDATE USING (auth.uid() = user_id);

View file

@ -2,18 +2,16 @@ import { useState } from "react";
import { ImageColorPicker } from "./ImageColorPicker";
import { ClickOutside } from "./ClickOutside";
import { StatusPicker } from "./StatusPicker";
import { Database } from "@ui/types/database.types";
interface Tablo {
id: number;
name: string;
image?: string;
color?: string;
status: "todo" | "in_progress" | "done";
}
type Tablo = Database["public"]["Tables"]["tablos"]["Row"];
type StatusType = "todo" | "in_progress" | "done";
interface CreateTabloModalProps {
onClose: () => void;
onCreate: (tabloData: Omit<Tablo, "id">) => void;
onCreate: (
tabloData: Omit<Tablo, "id" | "user_id" | "created_at" | "deleted_at">
) => void;
}
export const CreateTabloModal = ({
@ -26,9 +24,7 @@ export const CreateTabloModal = ({
"https://images.unsplash.com/photo-1553877522-43269d4ea984?w=400&h=250&fit=crop&crop=center"
);
const [selectedColor, setSelectedColor] = useState("bg-blue-500");
const [selectedStatus, setSelectedStatus] = useState<
"todo" | "in_progress" | "done"
>("todo");
const [selectedStatus, setSelectedStatus] = useState<StatusType>("todo");
const resetForm = () => {
setNewTabloName("");
@ -51,8 +47,8 @@ export const CreateTabloModal = ({
name: newTabloName.trim(),
status: selectedStatus,
...(creationMode === "image"
? { image: selectedImage }
: { color: selectedColor }),
? { image: selectedImage, color: null }
: { image: null, color: selectedColor }),
};
onCreate(tabloData);
resetForm();

View file

@ -0,0 +1,116 @@
import { ClickOutside } from "./ClickOutside";
import { Database } from "@ui/types/database.types";
type Tablo = Database["public"]["Tables"]["tablos"]["Row"];
interface DeleteTabloModalProps {
tablo: Tablo | null;
onClose: () => void;
onConfirm: (tabloId: number) => void;
isDeleting: boolean;
}
export const DeleteTabloModal = ({
tablo,
onClose,
onConfirm,
isDeleting,
}: DeleteTabloModalProps) => {
if (!tablo) return null;
const handleConfirm = () => {
onConfirm(tablo.id);
};
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<ClickOutside onClickOutside={onClose}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
{/* Header */}
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-red-600 dark:text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
></path>
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Supprimer le tablo
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Cette action est irréversible
</p>
</div>
</div>
{/* Content */}
<div className="mb-6">
<p className="text-gray-700 dark:text-gray-300 mb-3">
Êtes-vous sûr de vouloir supprimer le tablo{" "}
<span className="font-semibold text-gray-900 dark:text-white">
&ldquo;{tablo.name}&rdquo;
</span>{" "}
?
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Toutes les données associées à ce tablo seront perdues
définitivement.
</p>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3">
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
onClick={onClose}
disabled={isDeleting}
>
Annuler
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
onClick={handleConfirm}
disabled={isDeleting}
>
{isDeleting && (
<svg
className="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)}
{isDeleting ? "Suppression..." : "Supprimer"}
</button>
</div>
</div>
</ClickOutside>
</div>
);
};

View file

@ -2,43 +2,40 @@ import { ClickOutside } from "./ClickOutside";
import { useState } from "react";
import { ImageColorPicker } from "./ImageColorPicker";
import { StatusPicker } from "./StatusPicker";
import { Database } from "@ui/types/database.types";
interface Tablo {
id: number;
name: string;
image?: string;
color?: string;
status: "todo" | "in_progress" | "done";
}
type Tablo = Database["public"]["Tables"]["tablos"]["Row"];
type StatusType = "todo" | "in_progress" | "done";
interface TabloModalProps {
tablo: Tablo | null;
onEdit: (updatedTablo: Tablo) => void;
onClose: () => void;
onSave?: (updatedTablo: Tablo) => void;
}
export const TabloModal = ({ tablo, onClose, onSave }: TabloModalProps) => {
export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
const [editData, setEditData] = useState<Tablo | null>(tablo);
const [isEditingName, setIsEditingName] = useState(false);
const [creationMode, setCreationMode] = useState<"image" | "color">("color");
const [selectedColor, setSelectedColor] = useState("bg-blue-500");
const [selectedColor, setSelectedColor] = useState(
tablo?.color || "bg-blue-500"
);
const handleCancelEdit = () => {
setEditData(null);
};
const handleSaveEdit = () => {
if (editData && onSave) {
if (editData && onEdit) {
// Clear the unused field based on selection
const updatedTablo = {
...editData,
image: creationMode === "image" ? editData.image : undefined,
color: creationMode === "color" ? editData.color : undefined,
//TODO: image: creationMode === "image" ? editData.image : null,
color: creationMode === "color" ? selectedColor : null,
};
onSave(updatedTablo);
onEdit(updatedTablo);
}
setEditData(null);
};
if (!tablo) return null;
@ -93,7 +90,7 @@ export const TabloModal = ({ tablo, onClose, onSave }: TabloModalProps) => {
<div className="space-y-4 mb-4">
<div>
<StatusPicker
selectedStatus={currentData.status}
selectedStatus={currentData.status as StatusType}
setSelectedStatus={(status) =>
setEditData((prev) => (prev ? { ...prev, status } : null))
}

97
ui/src/hooks/tablos.ts Normal file
View file

@ -0,0 +1,97 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Database } from "@ui/types/database.types";
import { supabase } from "./auth";
import { useSession } from "@ui/contexts/SessionContext";
type Tablo = Database["public"]["Tables"]["tablos"];
type TabloInsert = Tablo["Insert"];
type TabloUpdate = Tablo["Update"];
// Fetch all tablos
export const useTablosList = () => {
return useQuery({
queryKey: ["tablos"],
queryFn: async () => {
const { data, error } = await supabase
.from("tablos")
.select("*")
.is("deleted_at", null)
.order("position", { ascending: true });
if (error) throw error;
return data;
},
});
};
// Fetch single tablo
export const useTablo = (id: number) => {
return useQuery({
queryKey: ["tablos", id],
queryFn: async () => {
const { data, error } = await supabase
.from("tablos")
.select("*")
.eq("id", id)
.is("deleted_at", null);
if (error) throw error;
return data[0];
},
});
};
// Create new tablo
export const useCreateTablo = () => {
const { session } = useSession();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (tablo: Omit<TabloInsert, "user_id">) => {
const { error } = await supabase.from("tablos").insert({
...tablo,
user_id: session?.user.id ?? "",
});
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
},
});
};
// Update tablo
export const useUpdateTablo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...tablo }: TabloUpdate & { id: number }) => {
const { error } = await supabase
.from("tablos")
.update(tablo)
.eq("id", id);
if (error) throw error;
},
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
queryClient.invalidateQueries({ queryKey: ["tablos", id] });
},
});
};
// Delete tablo (soft delete)
export const useDeleteTablo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
const { error } = await supabase
.from("tablos")
.update({ deleted_at: new Date().toISOString() })
.eq("id", id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
},
});
};

View file

@ -1,66 +1,30 @@
import { SignOutButton } from "@ui/components/SignOutButton";
import { CreateTabloModal } from "@ui/components/CreateTabloModal";
import { TabloModal } from "@ui/components/TabloModal";
import { DeleteTabloModal } from "@ui/components/DeleteTabloModal";
import { useState } from "react";
import {
useTablosList,
useCreateTablo,
useUpdateTablo,
useDeleteTablo,
} from "@ui/hooks/tablos";
import { Database } from "@ui/types/database.types";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
interface Tablo {
id: number;
name: string;
image?: string;
color?: string;
status: "todo" | "in_progress" | "done";
}
type Tablo = Database["public"]["Tables"]["tablos"]["Row"];
export const TabloPage = () => {
const [contextMenuTablo, setContextMenuTablo] = useState<number | null>(null);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [viewingTablo, setViewingTablo] = useState<Tablo | null>(null);
const [deletingTablo, setDeletingTablo] = useState<Tablo | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
// Sample tablo data - in a real app this would come from an API
const [tablos, setTablos] = useState<Tablo[]>([
{
id: 1,
name: "Projet Alpha",
image:
"https://images.unsplash.com/photo-1553877522-43269d4ea984?w=400&h=250&fit=crop&crop=center",
status: "in_progress",
},
{
id: 2,
name: "Marketing Q4",
image:
"https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&h=250&fit=crop&crop=center",
status: "done",
},
{
id: 3,
name: "Équipe Dev",
image:
"https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400&h=250&fit=crop&crop=center",
status: "todo",
},
{
id: 4,
name: "Budget 2024",
image:
"https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400&h=250&fit=crop&crop=center",
status: "in_progress",
},
{
id: 5,
name: "Roadmap",
image:
"https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=400&h=250&fit=crop&crop=center",
status: "todo",
},
{
id: 6,
name: "Support Client",
image:
"https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center",
status: "done",
},
]);
const { data: tablos, isLoading, error } = useTablosList();
const createTabloMutation = useCreateTablo();
const { mutateAsync: updateTablo } = useUpdateTablo();
const { mutateAsync: deleteTablo } = useDeleteTablo();
const menuItems = [
{ name: "Conversations" },
@ -76,42 +40,30 @@ export const TabloPage = () => {
setIsCreateModalOpen(false);
};
const createNewTablo = (tabloData: Omit<Tablo, "id">) => {
const newId = Math.max(...tablos.map((t) => t.id), 0) + 1;
const newTablo: Tablo = {
id: newId,
...tabloData,
};
setTablos([...tablos, newTablo]);
setIsCreateModalOpen(false);
const createNewTablo = async (
tabloData: Omit<Tablo, "id" | "user_id" | "created_at" | "deleted_at">
) => {
try {
await createTabloMutation.mutateAsync(tabloData);
setIsCreateModalOpen(false);
} catch (error) {
console.error("Error creating tablo:", error);
}
};
// Tablo movement functions
// Tablo movement functions - simplified for now, would need position field in DB
const moveTabloLeft = (tabloId: number) => {
const currentIndex = tablos.findIndex((t) => t.id === tabloId);
if (currentIndex > 0) {
const newTablos = [...tablos];
[newTablos[currentIndex - 1], newTablos[currentIndex]] = [
newTablos[currentIndex],
newTablos[currentIndex - 1],
];
setTablos(newTablos);
}
// TODO: Implement with proper position field in database
console.log("Moving tablo left:", tabloId);
};
const moveTabloRight = (tabloId: number) => {
const currentIndex = tablos.findIndex((t) => t.id === tabloId);
if (currentIndex < tablos.length - 1) {
const newTablos = [...tablos];
[newTablos[currentIndex], newTablos[currentIndex + 1]] = [
newTablos[currentIndex + 1],
newTablos[currentIndex],
];
setTablos(newTablos);
}
// TODO: Implement with proper position field in database
console.log("Moving tablo right:", tabloId);
};
const openTablo = (tabloId: number) => {
if (!tablos) return;
const tablo = tablos.find((t) => t.id === tabloId);
if (tablo) {
setViewingTablo(tablo);
@ -122,7 +74,7 @@ export const TabloPage = () => {
setViewingTablo(null);
};
const getStatusLabel = (status: Tablo["status"]) => {
const getStatusLabel = (status: string) => {
switch (status) {
case "todo":
return "À faire";
@ -135,7 +87,7 @@ export const TabloPage = () => {
}
};
const getStatusBadgeColor = (status: Tablo["status"]) => {
const getStatusBadgeColor = (status: string) => {
switch (status) {
case "todo":
return "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300";
@ -148,14 +100,144 @@ export const TabloPage = () => {
}
};
const changeTabloStatus = (tabloId: number, newStatus: Tablo["status"]) => {
setTablos(
tablos.map((tablo) =>
tablo.id === tabloId ? { ...tablo, status: newStatus } : tablo
)
);
const changeTabloStatus = async (tabloId: number, newStatus: string) => {
try {
await updateTablo({
id: tabloId,
status: newStatus,
});
setContextMenuTablo(null);
} catch (error) {
console.error("Error updating tablo status:", error);
}
};
const onEditTablo = (tablo: Tablo) => {
updateTablo(tablo, {
onSuccess: () => {
closeTabloModal();
},
});
};
const handleDeleteTablo = (tabloId: number) => {
if (!tablos) return;
const tablo = tablos.find((t) => t.id === tabloId);
if (tablo) {
setDeletingTablo(tablo);
}
};
const confirmDeleteTablo = async (tabloId: number) => {
setIsDeleting(true);
try {
await deleteTablo(tabloId);
setDeletingTablo(null);
} catch (error) {
console.error("Error deleting tablo:", error);
} finally {
setIsDeleting(false);
}
};
const cancelDeleteTablo = () => {
setDeletingTablo(null);
setIsDeleting(false);
};
// Show loading state
if (isLoading) {
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Tablos
</h1>
<div className="flex items-center gap-3">
<button
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Nouveau tablo</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-center items-center min-h-64">
<LoadingSpinner />
</div>
</main>
</div>
);
}
// Show error state
if (error) {
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Tablos
</h1>
<div className="flex items-center gap-3">
<button
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Nouveau tablo</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-center items-center min-h-64">
<div className="text-center">
<p className="text-red-600 dark:text-red-400 mb-2">
Erreur lors du chargement des tablos
</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">
{error instanceof Error
? error.message
: "Une erreur inconnue s'est produite"}
</p>
</div>
</div>
</main>
</div>
);
}
const renderTablo = (tablo: Tablo) => {
return (
<div
@ -174,7 +256,7 @@ export const TabloPage = () => {
}}
>
{/* Image or Color */}
<div className="relative h-56">
<div className="relative h-56 group">
{tablo.image ? (
<img
src={tablo.image}
@ -183,13 +265,40 @@ export const TabloPage = () => {
/>
) : (
<div
className={`w-full h-full ${tablo.color} flex items-center justify-center`}
className={`w-full h-full ${
tablo.color || "bg-gray-400"
} flex items-center justify-center`}
>
<h3 className="text-white font-bold text-2xl text-center px-4">
{tablo.name}
</h3>
</div>
)}
{/* Trash Icon */}
<button
className="absolute top-2 right-2 p-1.5 bg-red-500 hover:bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10"
onClick={(e) => {
e.stopPropagation();
handleDeleteTablo(tablo.id);
}}
title="Supprimer le tablo"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
</button>
</div>
{/* Content */}
@ -275,7 +384,6 @@ export const TabloPage = () => {
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "todo");
setContextMenuTablo(null);
}}
>
<span>À faire</span>
@ -288,7 +396,6 @@ export const TabloPage = () => {
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "in_progress");
setContextMenuTablo(null);
}}
>
<span>En cours</span>
@ -301,7 +408,6 @@ export const TabloPage = () => {
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "done");
setContextMenuTablo(null);
}}
>
<span>Terminé</span>
@ -326,6 +432,7 @@ export const TabloPage = () => {
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
onClick={openCreateModal}
disabled={createTabloMutation.isPending}
>
<svg
className="w-4 h-4"
@ -341,17 +448,50 @@ export const TabloPage = () => {
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Nouveau tablo</span>
<span>
{createTabloMutation.isPending ? "Création..." : "Nouveau tablo"}
</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{/* Render tablos */}
{tablos.map((tablo) => renderTablo(tablo))}
</div>
{tablos && tablos.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{/* Render tablos */}
{tablos.map((tablo) => renderTablo(tablo))}
</div>
) : (
<div className="flex justify-center items-center min-h-64">
<div className="text-center">
<p className="text-gray-500 dark:text-gray-400 mb-4">
Aucun tablo trouvé
</p>
<button
type="button"
className="flex items-center gap-1.5 px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Créer votre premier tablo</span>
</button>
</div>
</div>
)}
</div>
</main>
@ -365,7 +505,21 @@ export const TabloPage = () => {
{/* Tablo Details Modal */}
{!!viewingTablo && (
<TabloModal tablo={viewingTablo} onClose={closeTabloModal} />
<TabloModal
tablo={viewingTablo}
onEdit={onEditTablo}
onClose={closeTabloModal}
/>
)}
{/* Delete Tablo Modal */}
{!!deletingTablo && (
<DeleteTabloModal
tablo={deletingTablo}
onClose={cancelDeleteTablo}
onConfirm={confirmDeleteTablo}
isDeleting={isDeleting}
/>
)}
</div>
);

View file

@ -4,230 +4,263 @@ export type Json =
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
| Json[];
export type Database = {
public: {
Tables: {
devis: {
Row: {
client_email: string
created_at: string
date: string
due_date: string
id: string
items: Json
notes: string | null
number: string
status: Database["public"]["Enums"]["devis_status"]
subtotal: number
tax: number
terms: string | null
total: number
updated_at: string
user_id: string
}
client_email: string;
created_at: string;
date: string;
due_date: string;
id: string;
items: Json;
notes: string | null;
number: string;
status: Database["public"]["Enums"]["devis_status"];
subtotal: number;
tax: number;
terms: string | null;
total: number;
updated_at: string;
user_id: string;
};
Insert: {
client_email: string
created_at?: string
date: string
due_date: string
id?: string
items?: Json
notes?: string | null
number: string
status?: Database["public"]["Enums"]["devis_status"]
subtotal: number
tax: number
terms?: string | null
total: number
updated_at?: string
user_id: string
}
client_email: string;
created_at?: string;
date: string;
due_date: string;
id?: string;
items?: Json;
notes?: string | null;
number: string;
status?: Database["public"]["Enums"]["devis_status"];
subtotal: number;
tax: number;
terms?: string | null;
total: number;
updated_at?: string;
user_id: string;
};
Update: {
client_email?: string
created_at?: string
date?: string
due_date?: string
id?: string
items?: Json
notes?: string | null
number?: string
status?: Database["public"]["Enums"]["devis_status"]
subtotal?: number
tax?: number
terms?: string | null
total?: number
updated_at?: string
user_id?: string
}
Relationships: []
}
client_email?: string;
created_at?: string;
date?: string;
due_date?: string;
id?: string;
items?: Json;
notes?: string | null;
number?: string;
status?: Database["public"]["Enums"]["devis_status"];
subtotal?: number;
tax?: number;
terms?: string | null;
total?: number;
updated_at?: string;
user_id?: string;
};
Relationships: [];
};
feedbacks: {
Row: {
created_at: string | null
fd_type: string
id: number
message: string
user_id: string
}
created_at: string | null;
fd_type: string;
id: number;
message: string;
user_id: string;
};
Insert: {
created_at?: string | null
fd_type: string
id?: number
message: string
user_id: string
}
created_at?: string | null;
fd_type: string;
id?: number;
message: string;
user_id: string;
};
Update: {
created_at?: string | null
fd_type?: string
id?: number
message?: string
user_id?: string
}
Relationships: []
}
created_at?: string | null;
fd_type?: string;
id?: number;
message?: string;
user_id?: string;
};
Relationships: [];
};
profiles: {
Row: {
avatar_url: string | null
email: string | null
id: string
name: string | null
}
avatar_url: string | null;
email: string | null;
id: string;
name: string | null;
};
Insert: {
avatar_url?: string | null
email?: string | null
id: string
name?: string | null
}
avatar_url?: string | null;
email?: string | null;
id: string;
name?: string | null;
};
Update: {
avatar_url?: string | null
email?: string | null
id?: string
name?: string | null
}
Relationships: []
}
}
avatar_url?: string | null;
email?: string | null;
id?: string;
name?: string | null;
};
Relationships: [];
};
tablos: {
Row: {
color: string | null;
created_at: string | null;
deleted_at: string | null;
id: number;
image: string | null;
name: string;
status: string;
user_id: string;
};
Insert: {
color?: string | null;
created_at?: string | null;
deleted_at?: string | null;
id?: number;
image?: string | null;
name: string;
status?: string;
user_id: string;
};
Update: {
color?: string | null;
created_at?: string | null;
deleted_at?: string | null;
id?: number;
image?: string | null;
name?: string;
status?: string;
user_id?: string;
};
Relationships: [];
};
};
Views: {
[_ in never]: never
}
[_ in never]: never;
};
Functions: {
[_ in never]: never
}
[_ in never]: never;
};
Enums: {
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
}
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired";
};
CompositeTypes: {
[_ in never]: never
}
}
}
[_ in never]: never;
};
};
};
type DefaultSchema = Database[Extract<keyof Database, "public">]
type DefaultSchema = Database[Extract<keyof Database, "public">];
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof Database;
}
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
: never = never
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
Row: infer R;
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R;
}
? R
: never
: never;
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof Database;
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
: never = never
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
Insert: infer I;
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I;
}
? I
: never
: never;
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
schema: keyof Database;
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
: never = never
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
Update: infer U;
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U;
}
? U
: never
: never;
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof Database },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof Database
schema: keyof Database;
}
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
: never = never
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never;
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof Database },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database
schema: keyof Database;
}
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
: never = never
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never;
export const Constants = {
public: {
@ -235,4 +268,4 @@ export const Constants = {
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
},
},
} as const
} as const;