Implement planning
This commit is contained in:
parent
7b879ebfb8
commit
4941faa5ed
8 changed files with 859 additions and 220 deletions
|
|
@ -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
|
||||
|
|
|
|||
114
sql/14_create_events_table.sql
Normal file
114
sql/14_create_events_table.sql
Normal 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';
|
||||
22
sql/15_create_events_and_tablos_view.sql
Normal file
22
sql/15_create_events_and_tablos_view.sql
Normal 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';
|
||||
184
ui/src/components/CreateEventModal.tsx
Normal file
184
ui/src/components/CreateEventModal.tsx
Normal 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
234
ui/src/hooks/events.ts
Normal 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`);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
ui/src/types/events.types.ts
Normal file
22
ui/src/types/events.types.ts
Normal 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"
|
||||
>;
|
||||
Loading…
Reference in a new issue