Add availabilities
This commit is contained in:
parent
223a7d5cad
commit
90b7e04473
8 changed files with 1240 additions and 6 deletions
|
|
@ -7,13 +7,80 @@ export type Json =
|
|||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instanciate createClient with right options
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "12.2.3 (519615d)"
|
||||
PostgrestVersion: "13.0.4"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
availabilities: {
|
||||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
calendar_subscriptions: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
id: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id?: string
|
||||
token?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string
|
||||
|
|
@ -367,7 +434,10 @@ export type Database = {
|
|||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
time_range: {
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
sql/17_availabilities_table.sql
Normal file
69
sql/17_availabilities_table.sql
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
-- Create the availabilities table
|
||||
CREATE TABLE IF NOT EXISTS availabilities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
availabilities JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create an index for faster lookups by user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_availabilities_user_id ON availabilities(user_id);
|
||||
|
||||
-- Add unique constraint on user_id to ensure one availability record per user
|
||||
ALTER TABLE availabilities ADD CONSTRAINT unique_user_availabilities UNIQUE (user_id);
|
||||
|
||||
|
||||
-- Add trigger to update updated_at timestamp
|
||||
CREATE TRIGGER update_availabilities_updated_at
|
||||
BEFORE UPDATE ON availabilities
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE availabilities ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy to allow users to view their own availabilities
|
||||
CREATE POLICY "Users can view their own availabilities" ON availabilities
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to insert their own availabilities
|
||||
CREATE POLICY "Users can insert their own availabilities" ON availabilities
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to update their own availabilities
|
||||
CREATE POLICY "Users can update their own availabilities" ON availabilities
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid())
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to delete their own availabilities
|
||||
CREATE POLICY "Users can delete their own availabilities" ON availabilities
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
-- Add helpful comments
|
||||
COMMENT ON TABLE availabilities IS
|
||||
'User availability settings with Row Level Security';
|
||||
|
||||
COMMENT ON COLUMN availabilities.id IS
|
||||
'Primary key: auto-incrementing integer';
|
||||
|
||||
COMMENT ON COLUMN availabilities.user_id IS
|
||||
'Foreign key reference to auth.users(id)';
|
||||
|
||||
COMMENT ON COLUMN availabilities.availabilities IS
|
||||
'JSONB object containing availability settings for each day (0-6, where 0 is Monday). Each day has enabled status and time ranges.';
|
||||
|
||||
-- Rename the availabilities column to availability_data for clarity
|
||||
ALTER TABLE availabilities RENAME COLUMN availabilities TO availability_data;
|
||||
|
||||
-- Update the comment for the renamed column
|
||||
COMMENT ON COLUMN availabilities.availability_data IS
|
||||
'JSONB object containing availability settings for each day (0-6, where 0 is Monday). Each day has enabled status and time ranges.';
|
||||
194
ui/src/components/AvailabilityCard.tsx
Normal file
194
ui/src/components/AvailabilityCard.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { useState } from "react";
|
||||
import { Switch } from "@ui/ui-library/switch";
|
||||
import { Text } from "@ui/ui-library/text";
|
||||
import { Slider, SliderTack as SliderTrack } from "@ui/ui-library/slider";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { MinusIcon, PlusIcon } from "@ui/ui-library/icons";
|
||||
|
||||
interface TimeRange {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
interface AvailabilityCardProps {
|
||||
day: number;
|
||||
enabled: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
timeRanges: TimeRange[];
|
||||
onTimeRangesChange: (ranges: TimeRange[]) => void;
|
||||
}
|
||||
|
||||
const MINUTES_IN_DAY = 24 * 60;
|
||||
|
||||
const DAYS_OF_WEEK_DISPLAY = [
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi",
|
||||
"Dimanche",
|
||||
];
|
||||
|
||||
function timeToMinutes(time: string): number {
|
||||
const [hours, minutes] = time.split(":").map(Number);
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours.toString().padStart(2, "0")}:${mins
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function AvailabilityCard({
|
||||
day,
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
timeRanges,
|
||||
onTimeRangesChange,
|
||||
}: AvailabilityCardProps) {
|
||||
const dayDisplay = DAYS_OF_WEEK_DISPLAY[day];
|
||||
|
||||
const [selectedRangeIndex, setSelectedRangeIndex] = useState(0);
|
||||
|
||||
const handleAddRange = () => {
|
||||
// Find a free slot for the new range
|
||||
const sortedRanges = [...timeRanges].sort(
|
||||
(a, b) => timeToMinutes(a.start) - timeToMinutes(b.start)
|
||||
);
|
||||
let newStart = "09:00";
|
||||
let newEnd = "17:00";
|
||||
|
||||
for (let i = 0; i < sortedRanges.length; i++) {
|
||||
const currentRange = sortedRanges[i];
|
||||
const nextRange = sortedRanges[i + 1];
|
||||
|
||||
if (!nextRange) {
|
||||
// If this is the last range, add new range after it
|
||||
const currentEnd = timeToMinutes(currentRange.end);
|
||||
if (currentEnd + 120 <= MINUTES_IN_DAY) {
|
||||
// At least 2 hours before end of day
|
||||
newStart = minutesToTime(currentEnd + 30);
|
||||
newEnd = minutesToTime(Math.min(currentEnd + 240, MINUTES_IN_DAY)); // 4 hours or end of day
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const gap =
|
||||
timeToMinutes(nextRange.start) - timeToMinutes(currentRange.end);
|
||||
if (gap >= 120) {
|
||||
// At least 2 hours gap
|
||||
newStart = minutesToTime(timeToMinutes(currentRange.end) + 30);
|
||||
newEnd = minutesToTime(timeToMinutes(nextRange.start) - 30);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newRanges = [...timeRanges, { start: newStart, end: newEnd }];
|
||||
onTimeRangesChange(newRanges);
|
||||
setSelectedRangeIndex(newRanges.length - 1);
|
||||
};
|
||||
|
||||
const handleDeleteRange = (index: number) => {
|
||||
const newRanges = timeRanges.filter((_, i) => i !== index);
|
||||
onTimeRangesChange(newRanges);
|
||||
setSelectedRangeIndex(Math.min(selectedRangeIndex, newRanges.length - 1));
|
||||
};
|
||||
|
||||
const currentRange = timeRanges[selectedRangeIndex] || {
|
||||
start: "09:00",
|
||||
end: "17:00",
|
||||
};
|
||||
const value = [
|
||||
timeToMinutes(currentRange.start),
|
||||
timeToMinutes(currentRange.end),
|
||||
];
|
||||
|
||||
const handleChange = (newValue: number[]) => {
|
||||
const [start, end] = newValue;
|
||||
const newRanges = [...timeRanges];
|
||||
newRanges[selectedRangeIndex] = {
|
||||
start: minutesToTime(start),
|
||||
end: minutesToTime(end),
|
||||
};
|
||||
onTimeRangesChange(newRanges);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="text-lg font-semibold">{dayDisplay}</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
isSelected={enabled}
|
||||
onChange={onEnabledChange}
|
||||
className="data-[selected=true]:bg-primary"
|
||||
>
|
||||
<Text
|
||||
className={`font-medium text-sm ${
|
||||
enabled ? "text-gray-900" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{enabled ? "Disponible" : "Indisponible"}
|
||||
</Text>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5 flex-wrap items-center">
|
||||
{timeRanges.map((range, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 bg-gray-50 dark:bg-gray-900 rounded px-1.5 py-0.5"
|
||||
>
|
||||
<div className="flex gap-0.5 items-center text-xs whitespace-nowrap">
|
||||
<Text className="font-medium text-xs">{range.start}</Text>
|
||||
<Text className="text-gray-500 text-xs">-</Text>
|
||||
<Text className="font-medium text-xs">{range.end}</Text>
|
||||
</div>
|
||||
{timeRanges.length > 1 && (
|
||||
<Button
|
||||
onPress={() => handleDeleteRange(index)}
|
||||
isDisabled={!enabled}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
className="h-5 w-5 p-0 ml-0.5 border-rose-200 hover:border-rose-300 hover:bg-rose-50 dark:border-rose-800 dark:hover:border-rose-700 dark:hover:bg-rose-950/30 text-rose-600 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-300"
|
||||
>
|
||||
<MinusIcon className="size-2.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{timeRanges.length < 3 && (
|
||||
<Button
|
||||
onPress={() => handleAddRange()}
|
||||
isDisabled={!enabled}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 px-1.5 flex items-center gap-1 text-xs"
|
||||
>
|
||||
<PlusIcon className="size-2.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
minValue={0}
|
||||
maxValue={MINUTES_IN_DAY}
|
||||
step={30}
|
||||
isDisabled={!enabled}
|
||||
className="w-full"
|
||||
thumbLabels={["Début", "Fin"]}
|
||||
label={`${dayDisplay} (${currentRange.start} - ${currentRange.end})`}
|
||||
>
|
||||
<SliderTrack thumbLabels={["Début", "Fin"]} />
|
||||
</Slider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
ui/src/hooks/availabilities.ts
Normal file
89
ui/src/hooks/availabilities.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type TimeRange = {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
export type DayAvailability = {
|
||||
enabled: boolean;
|
||||
timeRanges: TimeRange[];
|
||||
};
|
||||
|
||||
export type WeeklyAvailability = {
|
||||
[key: number]: DayAvailability;
|
||||
};
|
||||
|
||||
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
|
||||
(acc, day) => {
|
||||
acc[day] = {
|
||||
enabled: true,
|
||||
timeRanges: [{ start: "09:00", end: "17:00" }],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as WeeklyAvailability
|
||||
);
|
||||
|
||||
export function useAvailabilities() {
|
||||
const { session } = useSession();
|
||||
|
||||
const { data: availabilities, isLoading } = useQuery<WeeklyAvailability>({
|
||||
queryKey: ["availabilities"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", session?.user.id)
|
||||
.limit(1);
|
||||
if (error) throw error;
|
||||
return data?.[0].availability_data as WeeklyAvailability;
|
||||
},
|
||||
enabled: !!session?.user.id,
|
||||
});
|
||||
|
||||
console.log("availabilities", availabilities);
|
||||
|
||||
const { mutate: updateAvailabilities, isPending: isUpdating } = useMutation({
|
||||
mutationFn: async (optionalAvailabilities: WeeklyAvailability) => {
|
||||
const newAvailabilities =
|
||||
optionalAvailabilities || DEFAULT_AVAILABILITIES;
|
||||
const { error } = await supabase.from("availabilities").upsert(
|
||||
{
|
||||
availability_data: newAvailabilities,
|
||||
user_id: session?.user.id,
|
||||
},
|
||||
{
|
||||
onConflict: "user_id",
|
||||
}
|
||||
);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["availabilities"] });
|
||||
},
|
||||
});
|
||||
|
||||
const [draftAvailabilities, setDraftAvailabilities] =
|
||||
useState<WeeklyAvailability | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (availabilities) {
|
||||
setDraftAvailabilities(availabilities);
|
||||
}
|
||||
}, [availabilities]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
updateAvailabilities,
|
||||
draftAvailabilities: draftAvailabilities || DEFAULT_AVAILABILITIES,
|
||||
setDraftAvailabilities,
|
||||
isUpdating,
|
||||
};
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import { ChantiersPage } from "@ui/pages/chantiers";
|
|||
import { ChatPage } from "@ui/pages/chat";
|
||||
import { FeedbackPage } from "@ui/pages/feedback";
|
||||
import { SupportPage } from "@ui/pages/support";
|
||||
import { AvailabilitiesPage } from "@ui/pages/availabilities";
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
// Protected routes
|
||||
|
|
@ -73,6 +74,10 @@ export const routes: RouteObject[] = [
|
|||
),
|
||||
children: [{ index: true }, { path: ":channelId" }],
|
||||
},
|
||||
{
|
||||
path: "availabilities",
|
||||
element: <AvailabilitiesPage />,
|
||||
},
|
||||
{
|
||||
path: "feedback",
|
||||
element: <FeedbackPage />,
|
||||
|
|
|
|||
169
ui/src/pages/availabilities.tsx
Normal file
169
ui/src/pages/availabilities.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { Strong, Text } from "@ui/ui-library/text";
|
||||
import { AvailabilityCard } from "@ui/components/AvailabilityCard";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
||||
import {
|
||||
DEFAULT_AVAILABILITIES,
|
||||
useAvailabilities,
|
||||
WeeklyAvailability,
|
||||
} from "@ui/hooks/availabilities";
|
||||
import { toast } from "@ui/ui-library/toast/toast-queue";
|
||||
|
||||
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
export function AvailabilitiesPage() {
|
||||
const {
|
||||
updateAvailabilities,
|
||||
isUpdating,
|
||||
draftAvailabilities,
|
||||
setDraftAvailabilities,
|
||||
} = useAvailabilities();
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Disponibilités</h2>
|
||||
<Strong className="text-gray-500 mt-2 text-xl">
|
||||
Définissez vos horaires de disponibilité pour chaque jour de la
|
||||
semaine
|
||||
</Strong>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="lg"
|
||||
onPress={() => {
|
||||
updateAvailabilities(DEFAULT_AVAILABILITIES);
|
||||
}}
|
||||
className="py-1"
|
||||
>
|
||||
Horaires de bureau (9h-17h)
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
const newAvailabilities: WeeklyAvailability = {};
|
||||
DAYS_OF_WEEK.forEach((day) => {
|
||||
newAvailabilities[day] = {
|
||||
enabled: false,
|
||||
timeRanges: [{ start: "09:00", end: "17:00" }],
|
||||
};
|
||||
});
|
||||
updateAvailabilities(newAvailabilities);
|
||||
}}
|
||||
className="py-1"
|
||||
>
|
||||
Tout désactiver
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="flex">
|
||||
<div className="flex-1 pr-6 border-r border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4 max-w-4xl min-h-min">
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow p-2"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<AvailabilityCard
|
||||
day={day}
|
||||
enabled={draftAvailabilities[day].enabled}
|
||||
onEnabledChange={(enabled) => {
|
||||
setDraftAvailabilities({
|
||||
...draftAvailabilities,
|
||||
[day]: {
|
||||
...draftAvailabilities[day],
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
}}
|
||||
timeRanges={draftAvailabilities[day].timeRanges}
|
||||
onTimeRangesChange={(ranges) => {
|
||||
setDraftAvailabilities({
|
||||
...draftAvailabilities,
|
||||
[day]: {
|
||||
...draftAvailabilities[day],
|
||||
timeRanges: ranges,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-80 pl-6 py-4">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Fuseau horaire</h3>
|
||||
<Text className="text-gray-500">
|
||||
Vos disponibilités sont affichées dans votre fuseau horaire
|
||||
local.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<Strong className="block mb-2">Votre fuseau horaire</Strong>
|
||||
<Text className="text-gray-500">
|
||||
{Intl.DateTimeFormat().resolvedOptions().timeZone}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400 mt-2">
|
||||
{new Date().toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}{" "}
|
||||
- Heure locale
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<Strong className="block mb-2">Information</Strong>
|
||||
<Text className="text-gray-500 text-sm">
|
||||
Les créneaux horaires seront automatiquement convertis dans le
|
||||
fuseau horaire de vos clients lorsqu'ils consulteront vos
|
||||
disponibilités.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 items-center border-t pt-4">
|
||||
{isUpdating && <LoadingSpinner />}
|
||||
<Button
|
||||
size="lg"
|
||||
variant="solid"
|
||||
isDisabled={isUpdating}
|
||||
onPress={() => {
|
||||
updateAvailabilities(draftAvailabilities, {
|
||||
onSuccess: () => {
|
||||
toast.add({
|
||||
title: "Succès",
|
||||
description: "Disponibilités enregistrées avec succès",
|
||||
type: "success",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.add({
|
||||
title: "Erreur",
|
||||
description:
|
||||
"Erreur lors de l'enregistrement des disponibilités",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isUpdating ? "Enregistrement..." : "Enregistrer les disponibilités"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,13 +7,80 @@ export type Json =
|
|||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instanciate createClient with right options
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "12.2.3 (519615d)"
|
||||
PostgrestVersion: "13.0.4"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
availabilities: {
|
||||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
calendar_subscriptions: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
id: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id?: string
|
||||
token?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string
|
||||
|
|
@ -367,7 +434,10 @@ export type Database = {
|
|||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
time_range: {
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
568
xtablo-expo/lib/database.types.ts
Normal file
568
xtablo-expo/lib/database.types.ts
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "13.0.4"
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
availabilities: {
|
||||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
calendar_subscriptions: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
id: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id: string
|
||||
token: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
tablo_id?: string
|
||||
token?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "events_and_tablos"
|
||||
referencedColumns: ["tablo_id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "calendar_subscriptions_tablo_id_fkey"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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: []
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
Update: {
|
||||
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
|
||||
}
|
||||
Insert: {
|
||||
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: []
|
||||
}
|
||||
tablo_access: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
granted_by: string
|
||||
id: number
|
||||
is_active: boolean | null
|
||||
is_admin: boolean | null
|
||||
tablo_id: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
granted_by: string
|
||||
id?: number
|
||||
is_active?: boolean | null
|
||||
is_admin?: boolean | null
|
||||
tablo_id: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
granted_by?: string
|
||||
id?: number
|
||||
is_active?: boolean | null
|
||||
is_admin?: boolean | null
|
||||
tablo_id?: string
|
||||
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"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "profiles"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
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"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
tablos: {
|
||||
Row: {
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
}
|
||||
Insert: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Update: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
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
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string | null
|
||||
image: string | null
|
||||
is_admin: boolean | null
|
||||
name: string | null
|
||||
position: number | null
|
||||
status: string | null
|
||||
user_id: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_access_user_id_from_profiles"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "profiles"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
generate_random_string: {
|
||||
Args: { length?: number }
|
||||
Returns: string
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
CompositeTypes: {
|
||||
time_range: {
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
Loading…
Reference in a new issue