Add exceptions + viz
This commit is contained in:
parent
d03022c21b
commit
a897b2d21e
11 changed files with 592 additions and 298 deletions
|
|
@ -18,6 +18,7 @@ export type Database = {
|
|||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
exceptions: Json | null
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
|
|
@ -25,6 +26,7 @@ export type Database = {
|
|||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
|
|
@ -32,6 +34,7 @@ export type Database = {
|
|||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
|
|
|
|||
|
|
@ -67,3 +67,10 @@ 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.';
|
||||
|
||||
-- Add exceptions column for date-specific availability overrides
|
||||
ALTER TABLE availabilities ADD COLUMN exceptions JSONB DEFAULT '[]';
|
||||
|
||||
-- Add comment for the exceptions column
|
||||
COMMENT ON COLUMN availabilities.exceptions IS
|
||||
'JSONB object containing date-specific availability exceptions that override regular availability settings. Keys are ISO date strings (YYYY-MM-DD), values contain exception type and optional time ranges.';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ interface AvailabilityCardProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
const MINUTES_IN_DAY = 24 * 60;
|
||||
const MAX_END_TIME_MINUTES = 23 * 60 + 30; // 23:30
|
||||
|
||||
const DAYS_OF_WEEK_DISPLAY = [
|
||||
|
|
@ -55,96 +54,6 @@ function minutesToTime(minutes: number): string {
|
|||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// TimeRangeSlider component for visualizing time ranges
|
||||
function TimeRangeSlider({ timeRanges }: { timeRanges: TimeRange[] }) {
|
||||
const sortedRanges = [...timeRanges].sort(
|
||||
(a, b) => timeToMinutes(a.start) - timeToMinutes(b.start)
|
||||
);
|
||||
|
||||
// Generate hour markers (every 2 hours from 6 AM to 10 PM)
|
||||
const hourMarkers = [];
|
||||
for (let hour = 6; hour <= 22; hour += 2) {
|
||||
hourMarkers.push({
|
||||
time: `${hour.toString().padStart(2, "0")}:00`,
|
||||
minutes: hour * 60,
|
||||
label: `${hour}h`,
|
||||
});
|
||||
}
|
||||
|
||||
const getPositionPercentage = (minutes: number) => {
|
||||
// Map 0-1440 minutes (24 hours) to 0-100%
|
||||
return (minutes / MINUTES_IN_DAY) * 100;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
<div className="relative h-8 bg-gradient-to-r from-gray-50 via-white to-gray-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
{/* Hour markers - subtle dots */}
|
||||
{hourMarkers.map((marker) => (
|
||||
<div
|
||||
key={marker.time}
|
||||
className="absolute top-1/2 transform -translate-y-1/2 w-1 h-1 bg-gray-300 dark:bg-gray-600 rounded-full"
|
||||
style={{ left: `${getPositionPercentage(marker.minutes)}%` }}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Time range bubbles */}
|
||||
{sortedRanges.map((range, index) => {
|
||||
const startMinutes = timeToMinutes(range.start);
|
||||
const endMinutes = timeToMinutes(range.end);
|
||||
const leftPercent = getPositionPercentage(startMinutes);
|
||||
const widthPercent = getPositionPercentage(endMinutes - startMinutes);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute top-1/2 transform -translate-y-1/2 h-5 bg-gradient-to-r from-primary/80 via-primary to-primary/80 dark:from-primary/70 dark:via-primary/80 dark:to-primary/70 rounded-xl shadow-md border border-primary/20 dark:border-primary/30 hover:shadow-lg transition-all duration-300 hover:scale-105"
|
||||
style={{
|
||||
left: `${leftPercent}%`,
|
||||
width: `${widthPercent}%`,
|
||||
minWidth: "20px",
|
||||
}}
|
||||
>
|
||||
{/* Inner glow effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/30 via-white/10 to-transparent rounded-xl" />
|
||||
|
||||
{/* Bubble highlight */}
|
||||
<div className="absolute top-0.5 left-1 right-1 h-1 bg-white/40 rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Start and end time markers - elegant */}
|
||||
<div className="absolute left-2 top-1/2 transform -translate-y-1/2">
|
||||
<div className="w-1.5 h-1.5 bg-gray-400 dark:bg-gray-500 rounded-full" />
|
||||
<span className="absolute top-2 left-1/2 transform -translate-x-1/2 text-[9px] text-gray-400 dark:text-gray-500 font-medium">
|
||||
0h
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
|
||||
<div className="w-1.5 h-1.5 bg-gray-400 dark:bg-gray-500 rounded-full" />
|
||||
<span className="absolute top-2 left-1/2 transform -translate-x-1/2 text-[9px] text-gray-400 dark:text-gray-500 font-medium">
|
||||
24h
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hour labels - refined */}
|
||||
<div className="relative mt-3 h-4">
|
||||
{hourMarkers.map((marker) => (
|
||||
<div
|
||||
key={marker.time}
|
||||
className="absolute text-xs font-semibold text-gray-500 dark:text-gray-400 transform -translate-x-1/2"
|
||||
style={{ left: `${getPositionPercentage(marker.minutes)}%` }}
|
||||
>
|
||||
{marker.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AvailabilityCard({
|
||||
day,
|
||||
enabled,
|
||||
|
|
@ -378,11 +287,6 @@ export function AvailabilityCard({
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time Slider Visualization */}
|
||||
{enabled && timeRanges.length > 0 && (
|
||||
<TimeRangeSlider timeRanges={timeRanges} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
127
ui/src/components/AvailabilityVisualization.tsx
Normal file
127
ui/src/components/AvailabilityVisualization.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { WeeklyAvailability } from "@ui/hooks/availabilities";
|
||||
import { Text } from "@ui/ui-library/text";
|
||||
|
||||
// Check if a time slot is available for a given day
|
||||
const isTimeSlotAvailable = (
|
||||
day: number,
|
||||
timeSlot: string,
|
||||
draftAvailabilities: WeeklyAvailability
|
||||
): boolean => {
|
||||
const dayAvailability = draftAvailabilities[day];
|
||||
if (!dayAvailability.enabled) return false;
|
||||
|
||||
const slotMinutes = timeToMinutes(timeSlot);
|
||||
return dayAvailability.timeRanges.some((range) => {
|
||||
const startMinutes = timeToMinutes(range.start);
|
||||
const endMinutes = timeToMinutes(range.end);
|
||||
return slotMinutes >= startMinutes && slotMinutes < endMinutes;
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to convert time string to minutes
|
||||
const timeToMinutes = (time: string): number => {
|
||||
const [hours, minutes] = time.split(":").map(Number);
|
||||
return hours * 60 + minutes;
|
||||
};
|
||||
|
||||
// Helper function to convert minutes to time string
|
||||
const 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")}`;
|
||||
};
|
||||
|
||||
// Generate time slots for visualization based on duration
|
||||
const generateTimeSlots = (intervalMinutes: number = 30) => {
|
||||
const slots = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += intervalMinutes) {
|
||||
slots.push(minutesToTime(hour * 60 + minute));
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
|
||||
const DAYS_OF_WEEK_DISPLAY = [
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi",
|
||||
"Dimanche",
|
||||
];
|
||||
|
||||
export const AvailabilityVisualization = ({
|
||||
draftAvailabilities,
|
||||
slotDurationMinutes = 30,
|
||||
}: {
|
||||
draftAvailabilities: WeeklyAvailability;
|
||||
slotDurationMinutes?: number;
|
||||
}) => {
|
||||
const timeSlots = generateTimeSlots(slotDurationMinutes);
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
{/* Weekly Calendar Header */}
|
||||
<div className="grid grid-cols-8 border-b-2 border-gray-200 dark:border-gray-600">
|
||||
<div className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-r border-gray-200 dark:border-gray-600">
|
||||
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">
|
||||
Heure
|
||||
</Text>
|
||||
</div>
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-r border-gray-200 dark:border-gray-600 last:border-r-0 text-center"
|
||||
>
|
||||
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">
|
||||
{DAYS_OF_WEEK_DISPLAY[day]}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Time Slots */}
|
||||
<div className="max-h-140 overflow-y-auto">
|
||||
{timeSlots.map((timeSlot) => {
|
||||
const timeMinutes = timeToMinutes(timeSlot);
|
||||
const hour = Math.floor(timeMinutes / 60);
|
||||
// Only show hours from 6 AM to 11 PM
|
||||
if (hour < 6 || hour > 23) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={timeSlot}
|
||||
className="grid grid-cols-8 border-b border-gray-100 dark:border-gray-700 hover:bg-slate-50/50 dark:hover:bg-slate-800/50 transition-colors duration-150"
|
||||
>
|
||||
<div className="p-3 border-r border-gray-200 dark:border-gray-600 bg-gradient-to-r from-slate-50/80 to-slate-100/80 dark:from-slate-800/80 dark:to-slate-900/80">
|
||||
<Text className="text-xs font-semibold text-slate-600 dark:text-slate-400">
|
||||
{timeSlot}
|
||||
</Text>
|
||||
</div>
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<div
|
||||
key={`${day}-${timeSlot}`}
|
||||
className="p-3 border-r border-gray-200 dark:border-gray-600 last:border-r-0 min-h-[3rem] flex items-center justify-center bg-gradient-to-br from-white to-slate-50/30 dark:from-gray-800 dark:to-slate-900/30"
|
||||
>
|
||||
{isTimeSlotAvailable(day, timeSlot, draftAvailabilities) ? (
|
||||
<div className="w-full h-8 bg-gradient-to-r from-emerald-400 via-emerald-500 to-emerald-600 dark:from-emerald-500 dark:via-emerald-600 dark:to-emerald-700 rounded-lg shadow-sm border border-emerald-300 dark:border-emerald-600 flex items-center justify-center group hover:shadow-md transition-all duration-200 hover:scale-105">
|
||||
<div className="w-3 h-3 bg-white/90 rounded-full shadow-sm group-hover:bg-white transition-colors duration-200"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-8 bg-gray-100 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 flex items-center justify-center opacity-50">
|
||||
<div className="w-2 h-2 bg-gray-300 dark:bg-gray-500 rounded-full"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import { queryClient } from "@ui/lib/api";
|
|||
import { supabase } from "@ui/hooks/auth";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
|
||||
export type TimeRange = {
|
||||
start: string;
|
||||
|
|
@ -18,6 +19,18 @@ export type WeeklyAvailability = {
|
|||
[key: number]: DayAvailability;
|
||||
};
|
||||
|
||||
export type Exception = {
|
||||
date: string;
|
||||
} & (
|
||||
| {
|
||||
type: "hours";
|
||||
hours: TimeRange;
|
||||
}
|
||||
| {
|
||||
type: "day";
|
||||
}
|
||||
);
|
||||
|
||||
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
|
||||
|
|
@ -34,7 +47,9 @@ export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
|
|||
export function useAvailabilities() {
|
||||
const { session } = useSession();
|
||||
|
||||
const { data: availabilities, isLoading } = useQuery<WeeklyAvailability>({
|
||||
const { data: availabilities, isLoading } = useQuery<
|
||||
Database["public"]["Tables"]["availabilities"]["Row"]
|
||||
>({
|
||||
queryKey: ["availabilities"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
|
|
@ -43,20 +58,35 @@ export function useAvailabilities() {
|
|||
.eq("user_id", session?.user.id)
|
||||
.limit(1);
|
||||
if (error) throw error;
|
||||
return data?.[0].availability_data as WeeklyAvailability;
|
||||
return data?.[0] as Database["public"]["Tables"]["availabilities"]["Row"];
|
||||
},
|
||||
enabled: !!session?.user.id,
|
||||
});
|
||||
|
||||
console.log("availabilities", availabilities);
|
||||
|
||||
const { mutate: updateAvailabilities, isPending: isUpdating } = useMutation({
|
||||
mutationFn: async (optionalAvailabilities: WeeklyAvailability) => {
|
||||
const newAvailabilities =
|
||||
optionalAvailabilities || DEFAULT_AVAILABILITIES;
|
||||
const { mutate: updateAvailabilities } = useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
updatedAvailabilities: WeeklyAvailability;
|
||||
newException?: Exception | null;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({
|
||||
updatedAvailabilities,
|
||||
newException = null,
|
||||
}: {
|
||||
updatedAvailabilities: WeeklyAvailability;
|
||||
newException?: Exception | null;
|
||||
}) => {
|
||||
const newAvailabilities = updatedAvailabilities || DEFAULT_AVAILABILITIES;
|
||||
const newExceptions = [
|
||||
...((availabilities?.exceptions as Exception[] | null) || []),
|
||||
newException || [],
|
||||
];
|
||||
const { error } = await supabase.from("availabilities").upsert(
|
||||
{
|
||||
availability_data: newAvailabilities,
|
||||
exceptions: newExceptions,
|
||||
user_id: session?.user.id,
|
||||
},
|
||||
{
|
||||
|
|
@ -74,16 +104,17 @@ export function useAvailabilities() {
|
|||
useState<WeeklyAvailability | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (availabilities) {
|
||||
setDraftAvailabilities(availabilities);
|
||||
if (availabilities?.availability_data) {
|
||||
setDraftAvailabilities(
|
||||
availabilities.availability_data as WeeklyAvailability
|
||||
);
|
||||
}
|
||||
}, [availabilities]);
|
||||
}, [availabilities?.availability_data]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
updateAvailabilities,
|
||||
draftAvailabilities: draftAvailabilities || DEFAULT_AVAILABILITIES,
|
||||
setDraftAvailabilities,
|
||||
isUpdating,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
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,
|
||||
Exception,
|
||||
useAvailabilities,
|
||||
WeeklyAvailability,
|
||||
} from "@ui/hooks/availabilities";
|
||||
import { toast } from "@ui/ui-library/toast/toast-queue";
|
||||
import { useState } from "react";
|
||||
import { Checkbox } from "@ui/ui-library/checkbox";
|
||||
import { PlusIcon } from "@ui/ui-library/icons";
|
||||
import { AvailabilityVisualization } from "@ui/components/AvailabilityVisualization";
|
||||
import { SaveIcon } from "lucide-react";
|
||||
import { DatePicker, DatePickerInput } from "@ui/ui-library/date-picker";
|
||||
import { Label } from "@ui/ui-library/field";
|
||||
import { DateValue, getLocalTimeZone, today } from "@internationalized/date";
|
||||
import { RadioGroup, Radios, Radio } from "@ui/ui-library/radio-group";
|
||||
|
||||
// Custom Modal Component
|
||||
interface CustomModalProps {
|
||||
|
|
@ -81,13 +88,12 @@ interface TimeRange {
|
|||
}
|
||||
|
||||
export function AvailabilitiesPage() {
|
||||
const {
|
||||
updateAvailabilities,
|
||||
isUpdating,
|
||||
draftAvailabilities,
|
||||
setDraftAvailabilities,
|
||||
} = useAvailabilities();
|
||||
const { updateAvailabilities, draftAvailabilities, setDraftAvailabilities } =
|
||||
useAvailabilities();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"reglages" | "visualisation">(
|
||||
"reglages"
|
||||
);
|
||||
const [copyModalOpen, setCopyModalOpen] = useState(false);
|
||||
const [sourceDayData, setSourceDayData] = useState<{
|
||||
day: number;
|
||||
|
|
@ -95,6 +101,10 @@ export function AvailabilitiesPage() {
|
|||
timeRanges: TimeRange[];
|
||||
} | null>(null);
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
||||
const [exceptionModalOpen, setExceptionModalOpen] = useState(false);
|
||||
const [exceptionType, setExceptionType] = useState<"day" | "hours">("day");
|
||||
const [exceptionDate, setExceptionDate] = useState<Date | null>(null);
|
||||
const [exceptionHours, setExceptionHours] = useState<TimeRange | null>(null);
|
||||
|
||||
const handleCopyToOtherDays = (
|
||||
sourceDay: number,
|
||||
|
|
@ -135,145 +145,195 @@ export function AvailabilitiesPage() {
|
|||
<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
|
||||
{activeTab === "reglages"
|
||||
? "Définissez vos horaires de disponibilité pour chaque jour de la semaine"
|
||||
: "Visualisez votre planning hebdomadaire"}
|
||||
</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>
|
||||
{activeTab === "reglages" && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="solid"
|
||||
className="[--btn-bg:var(--color-green-800)]"
|
||||
onPress={() => {
|
||||
updateAvailabilities(
|
||||
{
|
||||
updatedAvailabilities: draftAvailabilities,
|
||||
newException: null,
|
||||
},
|
||||
{
|
||||
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",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SaveIcon /> Enregistrer
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
onPress={() => setExceptionModalOpen(true)}
|
||||
className="[--btn-bg:var(--color-blue-800)]"
|
||||
>
|
||||
<PlusIcon /> Ajouter une exception
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
onPress={() => {
|
||||
updateAvailabilities({
|
||||
updatedAvailabilities: 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({
|
||||
updatedAvailabilities: newAvailabilities,
|
||||
});
|
||||
}}
|
||||
className="py-1"
|
||||
>
|
||||
Tout désactiver
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab("reglages")}
|
||||
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
|
||||
activeTab === "reglages"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
Réglages
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("visualisation")}
|
||||
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
|
||||
activeTab === "visualisation"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
Visualisation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-1 pr-6 border-r border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{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,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCopyToOtherDays={handleCopyToOtherDays}
|
||||
/>
|
||||
{activeTab === "reglages" && (
|
||||
<div className="flex items-start">
|
||||
<div className="flex-1 pr-6 border-r border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{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,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCopyToOtherDays={handleCopyToOtherDays}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-80 pl-6">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="w-80 pl-6">
|
||||
<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">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 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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{activeTab === "visualisation" && (
|
||||
<AvailabilityVisualization
|
||||
draftAvailabilities={draftAvailabilities}
|
||||
slotDurationMinutes={60}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Copy Modal */}
|
||||
|
|
@ -326,6 +386,126 @@ export function AvailabilitiesPage() {
|
|||
</div>
|
||||
</div>
|
||||
</CustomModal>
|
||||
|
||||
{/* Exception Modal */}
|
||||
<CustomModal
|
||||
isOpen={exceptionModalOpen}
|
||||
onClose={() => setExceptionModalOpen(false)}
|
||||
title="Ajouter une exception"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Text className="text-gray-600 dark:text-gray-400">
|
||||
Définissez une exception pour une date spécifique qui remplacera vos
|
||||
disponibilités habituelles.
|
||||
</Text>
|
||||
|
||||
<div className="space-y-4">
|
||||
<DatePicker
|
||||
minValue={today(getLocalTimeZone())}
|
||||
className="w-40"
|
||||
onChange={(value: DateValue | null) => {
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
setExceptionDate(value.toDate(getLocalTimeZone()));
|
||||
}}
|
||||
>
|
||||
<Label>Date de l'exception</Label>
|
||||
<DatePickerInput />
|
||||
</DatePicker>
|
||||
|
||||
<div className="space-y-2">
|
||||
<RadioGroup
|
||||
defaultValue="day"
|
||||
className="max-w-md"
|
||||
onChange={(value) => {
|
||||
setExceptionType(value as "day" | "hours");
|
||||
}}
|
||||
>
|
||||
<Label>Type d'exception</Label>
|
||||
<Radios>
|
||||
<Radio value="day">Indisponible toute la journée</Radio>
|
||||
<Radio value="hours">Horaires personnalisés</Radio>
|
||||
</Radios>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Custom Time Ranges (shown when custom is selected) */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Créneaux horaires (optionnel)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="time"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:text-white"
|
||||
defaultValue="09:00"
|
||||
onChange={(value: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setExceptionHours({
|
||||
start: value.target.value,
|
||||
end: exceptionHours?.end || "17:00",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Text className="text-gray-500">à</Text>
|
||||
<input
|
||||
type="time"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:text-white"
|
||||
defaultValue="17:00"
|
||||
onChange={(value: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setExceptionHours({
|
||||
start: exceptionHours?.start || "09:00",
|
||||
end: value.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
variant="outline"
|
||||
onPress={() => setExceptionModalOpen(false)}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
onPress={() => {
|
||||
setExceptionModalOpen(false);
|
||||
const exception: Exception =
|
||||
exceptionType === "hours"
|
||||
? {
|
||||
date: exceptionDate?.toISOString() || "",
|
||||
type: exceptionType,
|
||||
hours:
|
||||
exceptionHours ||
|
||||
({
|
||||
start: "09:00",
|
||||
end: "17:00",
|
||||
} as TimeRange),
|
||||
}
|
||||
: {
|
||||
date: exceptionDate?.toISOString() || "",
|
||||
type: exceptionType,
|
||||
};
|
||||
updateAvailabilities({
|
||||
updatedAvailabilities: draftAvailabilities,
|
||||
newException: exception,
|
||||
});
|
||||
toast.add({
|
||||
title: "Succès",
|
||||
description: "Exception ajoutée avec succès",
|
||||
type: "success",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Ajouter l'exception
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CustomModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type Database = {
|
|||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
exceptions: Json | null
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
|
|
@ -25,6 +26,7 @@ export type Database = {
|
|||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
|
|
@ -32,6 +34,7 @@ export type Database = {
|
|||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import {
|
|||
DateSegment,
|
||||
DateValue,
|
||||
composeRenderProps,
|
||||
} from 'react-aria-components';
|
||||
import { inputField } from './utils';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
} from "react-aria-components";
|
||||
import { inputField } from "./utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export interface DateFieldProps<T extends DateValue>
|
||||
extends RACDateFieldProps<T> {}
|
||||
|
|
@ -24,37 +24,44 @@ export function DateField<T extends DateValue>(props: DateFieldProps<T>) {
|
|||
inputField,
|
||||
// RAC does not set disable to date field when it is disable
|
||||
// So we have to style disable state for none input
|
||||
isDisabled && '[&>:not(input)]:opacity-50',
|
||||
className,
|
||||
isDisabled && "[&>:not(input)]:opacity-50",
|
||||
className
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type DateInputProps = Omit<RACDateInputProps, 'children'>;
|
||||
export type DateInputProps = Omit<RACDateInputProps, "children">;
|
||||
|
||||
export function DateInput(props: DateInputProps) {
|
||||
return (
|
||||
<RACDateInput
|
||||
{...props}
|
||||
data-ui="control"
|
||||
className={composeRenderProps(props.className, (className, renderProps) =>
|
||||
twMerge(
|
||||
'group flex w-full items-center rounded-md border border-input bg-transparent',
|
||||
|
||||
'[&:has([data-disabled=true])]:opacity-50',
|
||||
'[&:has([data-ui=date-segment][aria-readonly])]:bg-zinc-50',
|
||||
'dark:[&:has([data-ui=date-segment][aria-readonly])]:bg-white/10',
|
||||
'block min-w-[150px]',
|
||||
'text-base/6 sm:text-sm/6',
|
||||
'px-3',
|
||||
'py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
|
||||
renderProps.isInvalid && 'border-destructive',
|
||||
renderProps.isFocusWithin && 'border-ring ring-1 ring-ring',
|
||||
className,
|
||||
),
|
||||
className={composeRenderProps(
|
||||
props.className,
|
||||
(className, { isInvalid, isFocusWithin, isHovered, isDisabled }) =>
|
||||
twMerge(
|
||||
"group flex min-w-[150px] items-center",
|
||||
"w-full rounded-md text-base/6 shadow-sm outline-none sm:text-sm/6 dark:shadow-none",
|
||||
"px-2.5 py-2.5 sm:py-1.5",
|
||||
"ring ring-zinc-950/10 dark:ring-white/10",
|
||||
!isFocusWithin &&
|
||||
!isDisabled &&
|
||||
!isInvalid &&
|
||||
isHovered && [
|
||||
"[&:not(:has([data-ui=date-segment][aria-readonly]))]:ring-zinc-950/20",
|
||||
"dark:[&:not(:has([data-ui=date-segment][aria-readonly]))]:ring-white/20",
|
||||
],
|
||||
"[&:has([data-disabled=true])]:opacity-50",
|
||||
"[&:has([data-ui=date-segment][aria-readonly])]:bg-zinc-50",
|
||||
"dark:[&:has([data-ui=date-segment][aria-readonly])]:bg-white/5",
|
||||
isInvalid && "ring-red-600 dark:ring-red-600",
|
||||
isFocusWithin ? "ring-ring dark:ring-ring ring-2" : "",
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{(segment) => (
|
||||
|
|
@ -62,9 +69,9 @@ export function DateInput(props: DateInputProps) {
|
|||
data-ui="date-segment"
|
||||
segment={segment}
|
||||
className={twMerge(
|
||||
'inline rounded-sm px-0.5 caret-transparent outline-0 data-[type=literal]:px-0',
|
||||
'data-placeholder:italic data-placeholder:text-muted',
|
||||
'focus:bg-accent focus:text-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)] focus:data-placeholder:text-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)]',
|
||||
"inline rounded-sm px-0.5 caret-transparent outline-0 data-[type=literal]:px-0",
|
||||
"data-placeholder:text-muted data-placeholder:italic",
|
||||
"focus:bg-accent focus:text-[lch(from_var(--accent)_calc((49.44_-_l)_*_infinity)_0_0)] focus:data-placeholder:text-[lch(from_var(--accent)_calc((49.44_-_l)_*_infinity)_0_0)]"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
DatePicker as RACDatePicker,
|
||||
DatePickerProps as RACDatePickerProps,
|
||||
|
|
@ -7,15 +7,15 @@ import {
|
|||
useLocale,
|
||||
Group,
|
||||
composeRenderProps,
|
||||
} from 'react-aria-components';
|
||||
import { Button } from './button';
|
||||
import { Calendar, YearRange } from './calendar';
|
||||
import { DateInput, DateInputProps } from './date-field';
|
||||
import { Dialog } from './dialog';
|
||||
import { Popover } from './popover';
|
||||
import { inputField } from './utils';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { CalendarIcon } from './icons';
|
||||
} from "react-aria-components";
|
||||
import { Button } from "./button";
|
||||
import { Calendar, YearRange } from "./calendar";
|
||||
import { DateInput, DateInputProps } from "./date-field";
|
||||
import { Dialog } from "./dialog";
|
||||
import { Popover } from "./popover";
|
||||
import { inputField } from "./utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CalendarIcon } from "./icons/outline/calendar";
|
||||
|
||||
export interface DatePickerProps<T extends DateValue>
|
||||
extends RACDatePickerProps<T> {}
|
||||
|
|
@ -41,22 +41,21 @@ export function DatePickerInput({
|
|||
data-ui="control"
|
||||
{...props}
|
||||
className={[
|
||||
'group',
|
||||
'grid w-auto min-w-52',
|
||||
'grid-cols-[1fr_calc(theme(size.5)+20px)]',
|
||||
'sm:grid-cols-[1fr_calc(theme(size.4)+20px)]',
|
||||
].join(' ')}
|
||||
"group",
|
||||
"grid w-auto min-w-52",
|
||||
"grid-cols-[1fr_auto]",
|
||||
].join(" ")}
|
||||
>
|
||||
<DateInput
|
||||
{...props}
|
||||
className={composeRenderProps(props.className, (className) =>
|
||||
twMerge(
|
||||
'col-span-full',
|
||||
'row-start-1',
|
||||
'sm:pe-9',
|
||||
'pe-10',
|
||||
className,
|
||||
),
|
||||
"col-span-full",
|
||||
"row-start-1",
|
||||
"sm:pe-8",
|
||||
"pe-9",
|
||||
className
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
|
|
@ -65,18 +64,20 @@ export function DatePickerInput({
|
|||
isIconOnly
|
||||
data-ui="trigger"
|
||||
className={[
|
||||
'focus-visible:-outline-offset-1',
|
||||
'row-start-1',
|
||||
'-col-end-1',
|
||||
'place-self-center',
|
||||
'text-muted group-hover:text-foreground',
|
||||
].join(' ')}
|
||||
"me-1",
|
||||
"focus-visible:-outline-offset-1",
|
||||
"row-start-1",
|
||||
"-col-end-1",
|
||||
"place-self-center",
|
||||
"not-disabled:hover:bg-transparent",
|
||||
"not-disabled:not-hover:text-muted",
|
||||
].join(" ")}
|
||||
>
|
||||
<CalendarIcon />
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Popover placement="bottom" className="rounded-xl">
|
||||
<Popover placement="bottom" className="rounded-lg">
|
||||
<Dialog>
|
||||
<Calendar yearRange={yearRange} />
|
||||
</Dialog>
|
||||
|
|
@ -101,12 +102,12 @@ export function DatePickerButton({
|
|||
<Group data-ui="control">
|
||||
<Button
|
||||
className={twMerge(
|
||||
'border-input w-full min-w-52 flex-1 justify-between px-3 leading-6 font-normal',
|
||||
className,
|
||||
"w-full min-w-52 flex-1 justify-between px-3 leading-6 font-normal",
|
||||
className
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
{formattedDate === '' ? (
|
||||
{formattedDate === "" ? (
|
||||
<span className="text-muted">{children}</span>
|
||||
) : (
|
||||
<span>{formattedDate}</span>
|
||||
|
|
@ -118,7 +119,7 @@ export function DatePickerButton({
|
|||
<DateInput className="hidden" aria-hidden />
|
||||
</Group>
|
||||
|
||||
<Popover placement="bottom" className="rounded-xl">
|
||||
<Popover placement="bottom" className="rounded-lg">
|
||||
<Dialog>
|
||||
<Calendar />
|
||||
</Dialog>
|
||||
|
|
|
|||
28
ui/src/ui-library/icons/outline/calendar.tsx
Normal file
28
ui/src/ui-library/icons/outline/calendar.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Icon } from "../../icon";
|
||||
|
||||
export function CalendarIcon({
|
||||
"aria-label": arialLabel,
|
||||
...props
|
||||
}: React.JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<Icon aria-label={arialLabel}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
</svg>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export type Database = {
|
|||
Row: {
|
||||
availability_data: Json
|
||||
created_at: string
|
||||
exceptions: Json | null
|
||||
id: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
|
|
@ -25,6 +26,7 @@ export type Database = {
|
|||
Insert: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
|
|
@ -32,6 +34,7 @@ export type Database = {
|
|||
Update: {
|
||||
availability_data?: Json
|
||||
created_at?: string
|
||||
exceptions?: Json | null
|
||||
id?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
|
|
|
|||
Loading…
Reference in a new issue