Implement planning

This commit is contained in:
Arthur Belleville 2025-07-06 22:41:02 +02:00
parent 7b879ebfb8
commit 4941faa5ed
No known key found for this signature in database
8 changed files with 859 additions and 220 deletions

View file

@ -63,6 +63,67 @@ export type Database = {
}
Relationships: []
}
events: {
Row: {
created_at: string | null
created_by: string
deleted_at: string | null
description: string | null
end_time: string | null
id: string
start_date: string
start_time: string
tablo_id: string
title: string
}
Insert: {
created_at?: string | null
created_by: string
deleted_at?: string | null
description?: string | null
end_time?: string | null
id?: string
start_date: string
start_time: string
tablo_id: string
title: string
}
Update: {
created_at?: string | null
created_by?: string
deleted_at?: string | null
description?: string | null
end_time?: string | null
id?: string
start_date?: string
start_time?: string
tablo_id?: string
title?: string
}
Relationships: [
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "tablos"
referencedColumns: ["id"]
},
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "user_tablos"
referencedColumns: ["id"]
},
]
}
feedbacks: {
Row: {
created_at: string | null
@ -137,6 +198,13 @@ export type Database = {
user_id?: string
}
Relationships: [
{
foreignKeyName: "fk_tablo_access_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_tablo_access_tablo_id"
columns: ["tablo_id"]
@ -183,6 +251,13 @@ export type Database = {
tablo_id?: string
}
Relationships: [
{
foreignKeyName: "fk_tablo_invitations_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_tablo_invitations_tablo_id"
columns: ["tablo_id"]
@ -237,6 +312,21 @@ export type Database = {
}
}
Views: {
events_and_tablos: {
Row: {
description: string | null
end_time: string | null
event_id: string | null
start_date: string | null
start_time: string | null
tablo_color: string | null
tablo_id: string | null
tablo_name: string | null
tablo_status: string | null
title: string | null
}
Relationships: []
}
user_tablos: {
Row: {
access_level: string | null

View file

@ -0,0 +1,114 @@
-- Create events table for calendar/planning functionality
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY DEFAULT generate_random_string(24),
tablo_id TEXT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
start_date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME,
created_by UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
-- Foreign key constraint to tablos table
CONSTRAINT fk_events_tablo_id
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE,
-- Foreign key constraint to users table (auth.users)
CONSTRAINT fk_events_created_by
FOREIGN KEY (created_by) REFERENCES auth.users(id) ON DELETE CASCADE
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_events_tablo_id ON events(tablo_id);
CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by);
CREATE INDEX IF NOT EXISTS idx_events_start_date ON events(start_date);
CREATE INDEX IF NOT EXISTS idx_events_deleted_at ON events(deleted_at);
-- Enable Row Level Security
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
-- Policy to allow users to view events from tablos they have access to
CREATE POLICY "Users can view events from accessible tablos" ON events
FOR SELECT USING (
EXISTS (
SELECT 1 FROM user_tablos ut
JOIN events ON ut.id = events.tablo_id
WHERE events.deleted_at IS NULL
AND (
ut.user_id = (SELECT auth.uid())
)
)
);
-- Policy to allow users to insert events into tablos they have access to
CREATE POLICY "Users can insert events into accessible tablos" ON events
FOR INSERT WITH CHECK (
(SELECT auth.uid()) = created_by
AND EXISTS (
SELECT 1 FROM user_tablos ut
JOIN events ON ut.id = events.tablo_id
WHERE events.deleted_at IS NULL
AND (
ut.user_id = (SELECT auth.uid())
)
)
);
-- Policy to allow users to update their own events in accessible tablos
CREATE POLICY "Users can update their own events in accessible tablos" ON events
FOR UPDATE USING (
created_by = (SELECT auth.uid())
AND EXISTS (
SELECT 1 FROM user_tablos ut
JOIN events ON ut.id = events.tablo_id
WHERE events.deleted_at IS NULL
AND (
ut.user_id = (SELECT auth.uid())
)
)
) WITH CHECK (
created_by = (SELECT auth.uid())
AND EXISTS (
SELECT 1 FROM user_tablos ut
JOIN events ON ut.id = events.tablo_id
WHERE events.deleted_at IS NULL
AND (
ut.user_id = (SELECT auth.uid())
)
)
);
-- Policy to allow users to delete their own events in accessible tablos
CREATE POLICY "Users can delete their own events in accessible tablos" ON events
FOR DELETE USING (
created_by = (SELECT auth.uid())
AND EXISTS (
SELECT 1 FROM user_tablos ut
JOIN events ON ut.id = events.tablo_id
WHERE events.deleted_at IS NULL
AND (
ut.user_id = (SELECT auth.uid())
)
)
);
-- Add comments to document the table
COMMENT ON TABLE events IS
'Calendar events linked to tablos with Row Level Security';
COMMENT ON COLUMN events.id IS
'Primary key: random 24-character alphanumeric string';
COMMENT ON COLUMN events.tablo_id IS
'Foreign key reference to tablos.id (24-character string)';
COMMENT ON COLUMN events.start_date IS
'Date of the event (YYYY-MM-DD format)';
COMMENT ON COLUMN events.start_time IS
'Start time of the event (HH:MM format)';
COMMENT ON COLUMN events.end_time IS
'End time of the event (HH:MM format), optional';

View file

@ -0,0 +1,22 @@
CREATE OR REPLACE VIEW events_and_tablos
WITH (security_invoker)
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,
t.status as tablo_status
FROM events e
LEFT JOIN 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 ASC, e.start_time ASC;
-- Add comment to document the view
COMMENT ON VIEW events_and_tablos IS
'View that returns all events and their associated tablos parameters';

View file

@ -0,0 +1,184 @@
import { Event, EventInsert } from "@ui/types/events.types";
import { useState } from "react";
import { useTablosList } from "@ui/hooks/tablos";
import { useCreateEvent } from "@ui/hooks/events";
import { useUser } from "@ui/providers/UserStoreProvider";
interface EventModalProps {
date: Date;
onClose: () => void;
}
export const CreateEventModal = ({ date, onClose }: EventModalProps) => {
const user = useUser();
const { data: tablos, isLoading: tablosLoading } = useTablosList();
const createEvent = useCreateEvent();
const [createdEvent, setCreatedEvent] = useState<EventInsert>({
start_date: date?.toISOString().split("T")[0] || "",
start_time: date?.toISOString().split("T")[1] || "",
tablo_id: "",
title: "",
created_by: user.id,
});
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
{/* Header with colored accent */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 text-white">
<div className="flex items-center justify-between">
<h2 className="text-xl font-medium">Nouvel événement</h2>
<button
onClick={onClose}
className="text-white hover:text-gray-200 transition-colors"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="mt-2 text-blue-100 text-sm">
{date.toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</div>
</div>
{/* Form Content */}
<div className="p-6 space-y-6">
{/* Title Input */}
<div className="space-y-2">
<input
type="text"
value={createdEvent?.title}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
title: e.target.value,
} as Event)
}
className="w-full text-lg font-medium border-none outline-none bg-transparent text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 px-0"
placeholder="Ajouter un titre"
autoFocus
/>
<div className="border-b border-gray-200 dark:border-gray-700"></div>
</div>
{/* Tablo Selection */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
Tablo *
</label>
<select
value={createdEvent?.tablo_id}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
tablo_id: e.target.value,
} as Event)
}
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
disabled={tablosLoading}
>
<option value="">Sélectionner un tablo</option>
{tablos?.map((tablo) => (
<option key={tablo.id} value={tablo.id}>
{tablo.name}
</option>
))}
</select>
</div>
{/* Time Selection */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
Début
</label>
<input
type="time"
value={createdEvent?.start_time}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
start_time: e.target.value,
} as Event)
}
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
Fin
</label>
<input
type="time"
value={createdEvent?.end_time ?? ""}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
end_time: e.target.value,
} as Event)
}
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all"
/>
</div>
</div>
{/* Description */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-600 dark:text-gray-400">
Description
</label>
<textarea
value={createdEvent?.description ?? ""}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
description: e.target.value,
} as Event)
}
rows={3}
className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white resize-none transition-all"
placeholder="Ajouter une description (optionnel)"
/>
</div>
</div>
{/* Footer */}
<div className="bg-gray-50 dark:bg-gray-800 px-6 py-4 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 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
onClick={onClose}
>
Annuler
</button>
<button
type="button"
className="px-6 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm hover:shadow-md"
onClick={() =>
createEvent(createdEvent, { onSuccess: () => onClose() })
}
disabled={!createdEvent?.title.trim() || !createdEvent?.tablo_id}
>
Enregistrer
</button>
</div>
</div>
</div>
);
};

234
ui/src/hooks/events.ts Normal file
View file

@ -0,0 +1,234 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "./auth";
import { useUser } from "@ui/providers/UserStoreProvider";
import { toast } from "@ui/ui-library/toast/toast-queue";
import {
Event,
EventAndTablo,
EventInsert,
EventUpdate,
} from "@ui/types/events.types";
// Fetch events for a specific tablo
export const useEventsByTablo = (tabloId: string) => {
return useQuery({
queryKey: ["events", tabloId],
queryFn: async () => {
const { data, error } = await supabase
.from("events_and_tablos")
.select("*")
.eq("tablo_id", tabloId)
.order("start_date", { ascending: true })
.order("start_time", { ascending: true });
if (error) throw error;
return data as EventAndTablo[];
},
enabled: !!tabloId,
});
};
// Fetch events for a specific date range
export const useEventsByDateRange = (
tabloId: string,
startDate: string,
endDate: string
) => {
return useQuery({
queryKey: ["events", tabloId, "dateRange", startDate, endDate],
queryFn: async () => {
const { data, error } = await supabase
.from("events")
.select("*")
.eq("tablo_id", tabloId)
.gte("start_date", startDate)
.lte("start_date", endDate)
.is("deleted_at", null)
.order("start_date", { ascending: true })
.order("start_time", { ascending: true });
if (error) throw error;
return data as Event[];
},
enabled: !!tabloId && !!startDate && !!endDate,
});
};
// Fetch single event
export const useEvent = (eventId: string) => {
return useQuery({
queryKey: ["events", "single", eventId],
queryFn: async () => {
const { data, error } = await supabase
.from("events")
.select("*")
.eq("id", eventId)
.is("deleted_at", null)
.single();
if (error) throw error;
return data as Event;
},
enabled: !!eventId,
});
};
// Create new event
export const useCreateEvent = () => {
const user = useUser();
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: async (event: EventInsert) => {
const { data, error } = await supabase
.from("events")
.insert({
...event,
created_by: user.id,
})
.select()
.single();
if (error) throw error;
return data as Event;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["events", data.tablo_id] });
toast.add(
{
title: "Événement créé",
description: "L'événement a été créé avec succès",
type: "success",
},
{
timeout: 3000,
}
);
},
onError: (error) => {
console.error("Error creating event:", error);
toast.add(
{
title: "Erreur lors de la création",
description: "Impossible de créer l'événement",
type: "error",
},
{
timeout: 5000,
}
);
},
});
return mutate;
};
// Update event
export const useUpdateEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...updates }: EventUpdate & { id: string }) => {
const { data, error } = await supabase
.from("events")
.update(updates)
.eq("id", id)
.select()
.single();
if (error) throw error;
return data as Event;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["events", data.tablo_id] });
queryClient.invalidateQueries({
queryKey: ["events", "single", data.id],
});
toast.add(
{
title: "Événement modifié",
description: "L'événement a été modifié avec succès",
type: "success",
},
{
timeout: 3000,
}
);
},
onError: (error) => {
console.error("Error updating event:", error);
toast.add(
{
title: "Erreur lors de la modification",
description: "Impossible de modifier l'événement",
type: "error",
},
{
timeout: 5000,
}
);
},
});
};
// Delete event (soft delete)
export const useDeleteEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (eventId: string) => {
const { data, error } = await supabase
.from("events")
.update({ deleted_at: new Date().toISOString() })
.eq("id", eventId)
.select()
.single();
if (error) throw error;
return data as Event;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["events", data.tablo_id] });
queryClient.invalidateQueries({
queryKey: ["events", "single", data.id],
});
toast.add(
{
title: "Événement supprimé",
description: "L'événement a été supprimé avec succès",
type: "success",
},
{
timeout: 3000,
}
);
},
onError: (error) => {
console.error("Error deleting event:", error);
toast.add(
{
title: "Erreur lors de la suppression",
description: "Impossible de supprimer l'événement",
type: "error",
},
{
timeout: 5000,
}
);
},
});
};
// Utility function to format date for database
export const formatDateForDB = (date: Date): string => {
return date.toISOString().split("T")[0];
};
// Utility function to format time for database
export const formatTimeForDB = (date: Date): string => {
return date.toTimeString().split(" ")[0].slice(0, 5);
};
// Utility function to combine date and time into a Date object
export const combineDateAndTime = (date: string, time: string): Date => {
return new Date(`${date}T${time}:00`);
};

View file

@ -1,15 +1,7 @@
import { useState } from "react";
interface Event {
id: number;
title: string;
date: string;
time: string;
endTime?: string;
type: "meeting" | "task" | "reminder";
color: string;
description?: string;
}
import { useState, useEffect } from "react";
import { useTablosList } from "@ui/hooks/tablos";
import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events";
import { CreateEventModal } from "@ui/components/CreateEventModal";
type ViewType = "month" | "week" | "day";
@ -17,56 +9,26 @@ export const PlanningPage = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(new Date());
const [currentView, setCurrentView] = useState<ViewType>("month");
const [events, setEvents] = useState<Event[]>([
{
id: 1,
title: "Réunion équipe",
date: "2024-01-15",
time: "10:00",
endTime: "11:00",
type: "meeting",
color: "bg-blue-500",
description: "Discussion sur les objectifs du trimestre",
},
{
id: 2,
title: "Présentation client",
date: "2024-01-16",
time: "14:30",
endTime: "16:00",
type: "meeting",
color: "bg-red-500",
description: "Présentation du nouveau produit",
},
{
id: 3,
title: "Révision code",
date: "2024-01-17",
time: "09:00",
endTime: "10:30",
type: "task",
color: "bg-green-500",
description: "Code review des fonctionnalités développées",
},
{
id: 4,
title: "Appel client",
date: "2024-01-18",
time: "15:00",
endTime: "15:30",
type: "meeting",
color: "bg-purple-500",
},
]);
const [selectedTabloId, setSelectedTabloId] = useState<string>("");
// Fetch tablos
const { data: tablos, isLoading: tablosLoading } = useTablosList();
// Set default tablo if none selected
useEffect(() => {
if (tablos && tablos.length > 0 && !selectedTabloId) {
setSelectedTabloId(tablos[0].id);
}
}, [tablos, selectedTabloId]);
// Fetch events for selected tablo
const { data: events = [], isLoading: eventsLoading } =
useEventsByTablo(selectedTabloId);
const deleteEvent = useDeleteEvent();
// Modal state
const [isEventModalOpen, setIsEventModalOpen] = useState(false);
const [newEventTitle, setNewEventTitle] = useState("");
const [newEventTime, setNewEventTime] = useState("");
const [newEventEndTime, setNewEventEndTime] = useState("");
const [newEventType, setNewEventType] = useState<
"meeting" | "task" | "reminder"
>("meeting");
const [newEventDescription, setNewEventDescription] = useState("");
const monthNames = [
"Janvier",
@ -104,7 +66,7 @@ export const PlanningPage = () => {
const getEventsForDate = (date: Date) => {
const dateString = formatDate(date);
return events.filter((event) => event.date === dateString);
return events.filter((event) => event.start_date === dateString);
};
const navigateDate = (direction: number) => {
@ -125,33 +87,6 @@ export const PlanningPage = () => {
setSelectedDate(today);
};
const addEvent = () => {
if (newEventTitle.trim()) {
const newEvent: Event = {
id: Math.max(...events.map((e) => e.id), 0) + 1,
title: newEventTitle.trim(),
date: formatDate(selectedDate),
time: newEventTime || "09:00",
endTime: newEventEndTime || "",
type: newEventType,
color:
newEventType === "meeting"
? "bg-blue-500"
: newEventType === "task"
? "bg-green-500"
: "bg-yellow-500",
description: newEventDescription.trim(),
};
setEvents([...events, newEvent]);
setIsEventModalOpen(false);
setNewEventTitle("");
setNewEventTime("");
setNewEventEndTime("");
setNewEventType("meeting");
setNewEventDescription("");
}
};
const getViewTitle = () => {
if (currentView === "month") {
return `${
@ -284,11 +219,25 @@ export const PlanningPage = () => {
.slice(0, 3)
.map((event) => (
<div
key={event.id}
className={`text-xs px-2 py-1 rounded text-white ${event.color} truncate cursor-pointer hover:opacity-80`}
title={`${event.time} ${event.title}`}
key={event.event_id}
className={`text-xs px-2 py-1 rounded text-white ${event.tablo_color} truncate cursor-pointer hover:opacity-80 group relative`}
title={`${event.start_time} ${event.title}`}
onClick={(e) => {
e.stopPropagation();
setIsEventModalOpen(true);
}}
>
{event.title}
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity text-xs flex items-center justify-center hover:bg-red-600"
title="Supprimer l'événement"
>
×
</button>
</div>
))}
{getEventsForDate(day).length > 3 && (
@ -351,20 +300,21 @@ export const PlanningPage = () => {
}`}
onClick={() => {
setSelectedDate(day);
setNewEventTime(time);
setIsEventModalOpen(true);
}}
>
{getEventsForDate(day)
.filter((event) => event.time.startsWith(time.split(":")[0]))
.filter((event) =>
event.start_time.startsWith(time.split(":")[0])
)
.map((event) => (
<div
key={event.id}
className={`absolute left-1 right-1 top-1 p-1 rounded text-white ${event.color} text-xs`}
key={event.event_id}
className={`absolute left-1 right-1 top-1 p-1 rounded text-white ${event.tablo_color} text-xs`}
>
<div className="font-medium truncate">{event.title}</div>
<div className="opacity-75">
{event.time} - {event.endTime}
{event.start_time} - {event.end_time}
</div>
</div>
))}
@ -395,7 +345,7 @@ export const PlanningPage = () => {
key={time}
className="flex border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer relative min-h-[60px]"
onClick={() => {
setNewEventTime(time);
setSelectedDate(currentDate);
setIsEventModalOpen(true);
}}
>
@ -404,15 +354,17 @@ export const PlanningPage = () => {
</div>
<div className="flex-1 p-2 relative">
{getEventsForDate(currentDate)
.filter((event) => event.time.startsWith(time.split(":")[0]))
.filter((event) =>
event.start_time.startsWith(time.split(":")[0])
)
.map((event) => (
<div
key={event.id}
className={`p-2 rounded text-white ${event.color} mb-1`}
key={event.event_id}
className={`p-2 rounded text-white ${event.tablo_color} mb-1`}
>
<div className="font-medium">{event.title}</div>
<div className="text-sm opacity-75">
{event.time} - {event.endTime}
{event.start_time} - {event.end_time}
</div>
{event.description && (
<div className="text-sm mt-1 opacity-75">
@ -434,9 +386,32 @@ export const PlanningPage = () => {
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 min-h-screen">
<div className="p-4">
{/* Tablo Selector */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Tablo
</label>
<select
value={selectedTabloId}
onChange={(e) => setSelectedTabloId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
disabled={tablosLoading}
>
<option value="">
{tablosLoading ? "Chargement..." : "Sélectionner un tablo"}
</option>
{tablos?.map((tablo) => (
<option key={tablo.id} value={tablo.id}>
{tablo.name}
</option>
))}
</select>
</div>
<button
onClick={() => setIsEventModalOpen(true)}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium shadow-sm"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedTabloId}
>
+ Créer un événement
</button>
@ -566,130 +541,38 @@ export const PlanningPage = () => {
{/* Calendar Views */}
<div className="flex-1 p-4">
{currentView === "month" && renderMonthView()}
{currentView === "week" && renderWeekView()}
{currentView === "day" && renderDayView()}
{eventsLoading && selectedTabloId ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600 dark:text-gray-300">
Chargement des événements...
</span>
</div>
) : !selectedTabloId ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<p className="text-gray-500 dark:text-gray-400">
Sélectionnez un tablo pour voir les événements
</p>
</div>
</div>
) : (
<>
{currentView === "month" && renderMonthView()}
{currentView === "week" && renderWeekView()}
{currentView === "day" && renderDayView()}
</>
)}
</div>
</div>
</div>
{/* Event Modal */}
{isEventModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Nouvel événement
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Titre *
</label>
<input
type="text"
value={newEventTitle}
onChange={(e) => setNewEventTitle(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="Ajouter un titre"
autoFocus
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Début
</label>
<input
type="time"
value={newEventTime}
onChange={(e) => setNewEventTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Fin
</label>
<input
type="time"
value={newEventEndTime}
onChange={(e) => setNewEventEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Type
</label>
<select
value={newEventType}
onChange={(e) =>
setNewEventType(
e.target.value as "meeting" | "task" | "reminder"
)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="meeting">Réunion</option>
<option value="task">Tâche</option>
<option value="reminder">Rappel</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
value={newEventDescription}
onChange={(e) => setNewEventDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="Ajouter une description (optionnel)"
/>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 p-3 rounded-md">
📅{" "}
{selectedDate.toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors"
onClick={() => {
setIsEventModalOpen(false);
setNewEventTitle("");
setNewEventTime("");
setNewEventEndTime("");
setNewEventType("meeting");
setNewEventDescription("");
}}
>
Annuler
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
onClick={addEvent}
disabled={!newEventTitle.trim()}
>
Enregistrer
</button>
</div>
</div>
</div>
<CreateEventModal
date={selectedDate}
onClose={() => setIsEventModalOpen(false)}
/>
)}
</div>
);

View file

@ -63,6 +63,67 @@ export type Database = {
}
Relationships: []
}
events: {
Row: {
created_at: string | null
created_by: string
deleted_at: string | null
description: string | null
end_time: string | null
id: string
start_date: string
start_time: string
tablo_id: string
title: string
}
Insert: {
created_at?: string | null
created_by: string
deleted_at?: string | null
description?: string | null
end_time?: string | null
id?: string
start_date: string
start_time: string
tablo_id: string
title: string
}
Update: {
created_at?: string | null
created_by?: string
deleted_at?: string | null
description?: string | null
end_time?: string | null
id?: string
start_date?: string
start_time?: string
tablo_id?: string
title?: string
}
Relationships: [
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "tablos"
referencedColumns: ["id"]
},
{
foreignKeyName: "fk_events_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "user_tablos"
referencedColumns: ["id"]
},
]
}
feedbacks: {
Row: {
created_at: string | null
@ -137,6 +198,13 @@ export type Database = {
user_id?: string
}
Relationships: [
{
foreignKeyName: "fk_tablo_access_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_tablo_access_tablo_id"
columns: ["tablo_id"]
@ -183,6 +251,13 @@ export type Database = {
tablo_id?: string
}
Relationships: [
{
foreignKeyName: "fk_tablo_invitations_tablo_id"
columns: ["tablo_id"]
isOneToOne: false
referencedRelation: "events_and_tablos"
referencedColumns: ["tablo_id"]
},
{
foreignKeyName: "fk_tablo_invitations_tablo_id"
columns: ["tablo_id"]
@ -237,6 +312,21 @@ export type Database = {
}
}
Views: {
events_and_tablos: {
Row: {
description: string | null
end_time: string | null
event_id: string | null
start_date: string | null
start_time: string | null
tablo_color: string | null
tablo_id: string | null
tablo_name: string | null
tablo_status: string | null
title: string | null
}
Relationships: []
}
user_tablos: {
Row: {
access_level: string | null

View file

@ -0,0 +1,22 @@
import { Tables, TablesInsert, TablesUpdate } from "@ui/types/database.types";
import { RemoveNullFromObject } from "@ui/types/removeNull";
export type Event = RemoveNullFromObject<
Tables<"events">,
"created_at" | "end_time"
>;
export type EventInsert = TablesInsert<"events">;
export type EventUpdate = TablesUpdate<"events">;
export type EventAndTablo = RemoveNullFromObject<
Tables<"events_and_tablos">,
| "event_id"
| "tablo_id"
| "tablo_name"
| "tablo_color"
| "tablo_status"
| "start_time"
| "end_time"
| "title"
| "start_date"
>;