feat: add last sync and health
This commit is contained in:
parent
74f31a723a
commit
a89546c54e
7 changed files with 170 additions and 146 deletions
|
|
@ -24,6 +24,9 @@ import Error from "~/components/error";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { useColles, type User } from "~/lib/api";
|
import { useColles, type User } from "~/lib/api";
|
||||||
import TabContent from "~/components/home/tab-content";
|
import TabContent from "~/components/home/tab-content";
|
||||||
|
import { MainLayout } from "~/layout";
|
||||||
|
import { forceReload } from "~/lib/utils";
|
||||||
|
import { SyncButton } from "../sync-status";
|
||||||
|
|
||||||
export default function Home({ user }: { user: User }) {
|
export default function Home({ user }: { user: User }) {
|
||||||
// Handle query parameters
|
// Handle query parameters
|
||||||
|
|
@ -62,8 +65,15 @@ export default function Home({ user }: { user: User }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch colles from API
|
// Fetch colles from API
|
||||||
const { studentColles, classColles, favoriteColles, error, isLoading } =
|
const {
|
||||||
useColles(startDate);
|
studentColles,
|
||||||
|
classColles,
|
||||||
|
favoriteColles,
|
||||||
|
healthyUntil,
|
||||||
|
lastSync,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
} = useColles(startDate);
|
||||||
|
|
||||||
// Error handling (after all hooks)
|
// Error handling (after all hooks)
|
||||||
if (error)
|
if (error)
|
||||||
|
|
@ -143,106 +153,116 @@ export default function Home({ user }: { user: User }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 pb-20 md:pb-0">
|
<MainLayout
|
||||||
{/* Tabs */}
|
header={
|
||||||
<Tabs
|
<>
|
||||||
defaultValue="all"
|
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||||
value={activeTab}
|
Khollisé - {user.className} ⚔️
|
||||||
onValueChange={setActiveTab}
|
</h1>
|
||||||
className="max-w-md w-full"
|
<SyncButton healthyUntil={healthyUntil} lastSync={lastSync} />
|
||||||
>
|
</>
|
||||||
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
|
}
|
||||||
<TabsTrigger value="you" className={tabsStyle}>
|
>
|
||||||
<UserIcon className="h-4 w-4" />
|
<div className="space-y-6 pb-20 md:pb-0">
|
||||||
Vous
|
{/* Tabs */}
|
||||||
</TabsTrigger>
|
<Tabs
|
||||||
{/* <TabsTrigger value="favorites" className={tabsStyle}>
|
defaultValue="all"
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="max-w-md w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
|
||||||
|
<TabsTrigger value="you" className={tabsStyle}>
|
||||||
|
<UserIcon className="h-4 w-4" />
|
||||||
|
Vous
|
||||||
|
</TabsTrigger>
|
||||||
|
{/* <TabsTrigger value="favorites" className={tabsStyle}>
|
||||||
<Star className="h-4 w-4" />
|
<Star className="h-4 w-4" />
|
||||||
Favoris
|
Favoris
|
||||||
</TabsTrigger> */}
|
</TabsTrigger> */}
|
||||||
<TabsTrigger value="class" className={tabsStyle}>
|
<TabsTrigger value="class" className={tabsStyle}>
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
Classe
|
Classe
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Week Navigation */}
|
{/* Week Navigation */}
|
||||||
<div className="mb-0">
|
<div className="mb-0">
|
||||||
<div className="flex flex-row items-center justify-between gap-2">
|
<div className="flex flex-row items-center justify-between gap-2">
|
||||||
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
|
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
|
||||||
<ChevronLeft className="h-10 w-10" />
|
<ChevronLeft className="h-10 w-10" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<DatePickerWithRange
|
<DatePickerWithRange
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
setStartDate={setStartDate}
|
setStartDate={setStartDate}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleNextWeek}>
|
||||||
|
<ChevronRight className="h-10 w-10" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={handleNextWeek}>
|
</div>
|
||||||
<ChevronRight className="h-10 w-10" />
|
|
||||||
|
{/* Filter component */}
|
||||||
|
<div className="flex gap-2 pb-0 pt-2">
|
||||||
|
<Select value={subjectFilter} onValueChange={setSubject}>
|
||||||
|
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
||||||
|
<SelectValue placeholder="Matière" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toutes</SelectItem>
|
||||||
|
{subjects.map((subject) => (
|
||||||
|
<SelectItem key={subject} value={subject}>
|
||||||
|
{subject}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={examinerFilter} onValueChange={setExaminer}>
|
||||||
|
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
||||||
|
<SelectValue placeholder="Colleur" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Tous</SelectItem>
|
||||||
|
{examiners.map((examiner) => (
|
||||||
|
<SelectItem key={examiner} value={examiner}>
|
||||||
|
{examiner}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full dark:bg-input/30 text-primary font-normal"
|
||||||
|
onClick={toggleSort}
|
||||||
|
>
|
||||||
|
{sorted == "asc" ? (
|
||||||
|
<SortAsc className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<SortDesc className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
Trier
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter component */}
|
{/* Your Colles Tab */}
|
||||||
<div className="flex gap-2 pb-0 pt-2">
|
{activeTab === "you" && (
|
||||||
<Select value={subjectFilter} onValueChange={setSubject}>
|
<TabContent
|
||||||
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
tabTitle="Vos colles"
|
||||||
<SelectValue placeholder="Matière" />
|
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
|
||||||
</SelectTrigger>
|
isLoading={isLoading}
|
||||||
<SelectContent>
|
isSorted={sorted === "desc"}
|
||||||
<SelectItem value="all">Toutes</SelectItem>
|
colles={applyFilters(studentColles)}
|
||||||
{subjects.map((subject) => (
|
preferences={user.preferences}
|
||||||
<SelectItem key={subject} value={subject}>
|
/>
|
||||||
{subject}
|
)}
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select value={examinerFilter} onValueChange={setExaminer}>
|
{/* Favorites Tab
|
||||||
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
|
||||||
<SelectValue placeholder="Colleur" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous</SelectItem>
|
|
||||||
{examiners.map((examiner) => (
|
|
||||||
<SelectItem key={examiner} value={examiner}>
|
|
||||||
{examiner}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="rounded-full dark:bg-input/30 text-primary font-normal"
|
|
||||||
onClick={toggleSort}
|
|
||||||
>
|
|
||||||
{sorted == "asc" ? (
|
|
||||||
<SortAsc className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<SortDesc className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
Trier
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Your Colles Tab */}
|
|
||||||
{activeTab === "you" && (
|
|
||||||
<TabContent
|
|
||||||
tabTitle="Vos colles"
|
|
||||||
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
|
|
||||||
isLoading={isLoading}
|
|
||||||
isSorted={sorted === "desc"}
|
|
||||||
colles={applyFilters(studentColles)}
|
|
||||||
preferences={user.preferences}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Favorites Tab
|
|
||||||
{activeTab === "favorites" && (
|
{activeTab === "favorites" && (
|
||||||
<TabContent
|
<TabContent
|
||||||
tabTitle="Vos favoris"
|
tabTitle="Vos favoris"
|
||||||
|
|
@ -252,20 +272,21 @@ export default function Home({ user }: { user: User }) {
|
||||||
/>
|
/>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* Class Colles Tab */}
|
{/* Class Colles Tab */}
|
||||||
{activeTab === "class" && (
|
{activeTab === "class" && (
|
||||||
<TabContent
|
<TabContent
|
||||||
tabTitle="Les colles de la classe"
|
tabTitle="Les colles de la classe"
|
||||||
emptyCollesText="Aucune colle trouvée."
|
emptyCollesText="Aucune colle trouvée."
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isSorted={sorted === "desc"}
|
isSorted={sorted === "desc"}
|
||||||
colles={applyFilters(classColles)}
|
colles={applyFilters(classColles)}
|
||||||
preferences={user.preferences}
|
preferences={user.preferences}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Navigation for Mobile */}
|
{/* Bottom Navigation for Mobile */}
|
||||||
<BottomNavigation activeId="colles" />
|
<BottomNavigation activeId="colles" />
|
||||||
</div>
|
</div>
|
||||||
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,18 @@ import {
|
||||||
import { Cloud, CloudOff, Wifi, WifiOff } from "lucide-react";
|
import { Cloud, CloudOff, Wifi, WifiOff } from "lucide-react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
export function SyncButton() {
|
export function SyncButton({
|
||||||
const [isSync, setIsSync] = useState(true);
|
healthyUntil,
|
||||||
|
lastSync,
|
||||||
|
}: {
|
||||||
|
healthyUntil: Date;
|
||||||
|
lastSync: Date;
|
||||||
|
}) {
|
||||||
|
const healthyUntilDate = new Date(healthyUntil) || new Date(0);
|
||||||
|
const lastSyncDate = new Date(lastSync) || new Date(0);
|
||||||
|
// Determine if the sync is healthy based on the healthyUntil date
|
||||||
|
const isSync = healthyUntilDate > new Date();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const lastSync = new Date(); // TODO: Replace with actual last sync date
|
|
||||||
|
|
||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
if (isSync) return <Cloud className="w-6! h-6!" />;
|
if (isSync) return <Cloud className="w-6! h-6!" />;
|
||||||
|
|
@ -28,11 +36,12 @@ export function SyncButton() {
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diff = now.getTime() - date.getTime();
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
const minutes = Math.floor(diff / 60000);
|
const minutes = Math.floor(diff / 60000);
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
if (minutes < 1) return "A l'instant";
|
if (minutes < 1) return `il y a ${seconds}s`;
|
||||||
if (minutes < 60) return `il y a ${minutes}m`;
|
if (minutes < 60) return `il y a ${minutes}m`;
|
||||||
if (hours < 24) return `il y a ${hours}h`;
|
if (hours < 24) return `il y a ${hours}h`;
|
||||||
return `il y a ${days}j`;
|
return `il y a ${days}j`;
|
||||||
|
|
@ -61,26 +70,26 @@ export function SyncButton() {
|
||||||
) : (
|
) : (
|
||||||
<WifiOff className="h-4 w-4" />
|
<WifiOff className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium">
|
<span className="font-medium">Status</span>
|
||||||
Status
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto px-2 py-1 rounded-full text-xs font-medium",
|
"ml-auto px-2 py-1 rounded-full text-xs font-medium",
|
||||||
isSync
|
isSync
|
||||||
? "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
? "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||||
: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
: "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSync ? "Connecté" : "Erreur"}
|
{isSync ? "Connecté" : "Hors ligne"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Dernière mis à jour :</span>
|
<span className="text-muted-foreground">
|
||||||
<span>{formatLastSync(lastSync)}</span>
|
Dernière mis à jour :
|
||||||
|
</span>
|
||||||
|
<span>{formatLastSync(lastSyncDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO:
|
{/* TODO:
|
||||||
|
|
|
||||||
|
|
@ -51,18 +51,18 @@ export function AuthLayout({
|
||||||
|
|
||||||
export function MainLayout({
|
export function MainLayout({
|
||||||
children,
|
children,
|
||||||
page
|
header
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
page: React.ReactNode;
|
header: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<main className="container mx-auto py-8 px-4">
|
<main className="container mx-auto py-8 px-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
{children}
|
{header}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{page}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,8 @@ interface CollePayload {
|
||||||
classColles: Colle[];
|
classColles: Colle[];
|
||||||
studentColles: Colle[];
|
studentColles: Colle[];
|
||||||
favoriteColles: Colle[];
|
favoriteColles: Colle[];
|
||||||
|
healthyUntil: Date;
|
||||||
|
lastSync: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useColles = (startDate: DateTime) => {
|
export const useColles = (startDate: DateTime) => {
|
||||||
|
|
@ -206,7 +208,9 @@ export const useColles = (startDate: DateTime) => {
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
staleTime: 0,
|
staleTime: Duration.fromObject({
|
||||||
|
hours: 1, // 1 hour
|
||||||
|
}).toMillis(),
|
||||||
gcTime: Duration.fromObject({
|
gcTime: Duration.fromObject({
|
||||||
days: 3, // 3 days
|
days: 3, // 3 days
|
||||||
}).toMillis(),
|
}).toMillis(),
|
||||||
|
|
@ -222,6 +226,8 @@ export const useColles = (startDate: DateTime) => {
|
||||||
classColles: [],
|
classColles: [],
|
||||||
studentColles: [],
|
studentColles: [],
|
||||||
favoriteColles: [],
|
favoriteColles: [],
|
||||||
|
healthyUntil: new Date(0),
|
||||||
|
lastSync: new Date(0),
|
||||||
},
|
},
|
||||||
data || {}
|
data || {}
|
||||||
) as CollePayload;
|
) as CollePayload;
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,11 @@ import {
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
MapPinHouse,
|
MapPinHouse,
|
||||||
Users,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Share2,
|
Share2,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import UserDropdown from "~/components/user-dropdown";
|
|
||||||
import ColleDetailsSkeleton from "~/components/details/skeleton-details";
|
import ColleDetailsSkeleton from "~/components/details/skeleton-details";
|
||||||
import AttachmentItem from "~/components/details/attachment";
|
import AttachmentItem from "~/components/details/attachment";
|
||||||
import Error from "~/components/error";
|
import Error from "~/components/error";
|
||||||
|
|
@ -134,9 +132,6 @@ export default function ColleDetailPage() {
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Retour
|
Retour
|
||||||
</Button>
|
</Button>
|
||||||
<div className="hidden md:block">
|
|
||||||
<UserDropdown user={user} />
|
|
||||||
</div>
|
|
||||||
<div className="flex md:hidden items-center gap-2">
|
<div className="flex md:hidden items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -150,7 +145,6 @@ export default function ColleDetailPage() {
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">Recharger</span>
|
<span className="sr-only">Recharger</span>
|
||||||
</Button>
|
</Button>
|
||||||
<UserDropdown user={user} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ import { Navigate } from "react-router";
|
||||||
import Error from "~/components/error";
|
import Error from "~/components/error";
|
||||||
import HomePage from "~/components/home";
|
import HomePage from "~/components/home";
|
||||||
import Loader from "~/components/loader";
|
import Loader from "~/components/loader";
|
||||||
import { SyncButton } from "~/components/sync-status";
|
|
||||||
import { MainLayout } from "~/layout";
|
|
||||||
import { AUTH_ERROR, useUser } from "~/lib/api";
|
import { AUTH_ERROR, useUser } from "~/lib/api";
|
||||||
import { forceReload } from "~/lib/utils";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, isLoading, error } = useUser();
|
const { user, isLoading, error } = useUser();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
@ -20,12 +17,5 @@ export default function Home() {
|
||||||
return <Error message={error.message} />;
|
return <Error message={error.message} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <HomePage user={user} />;
|
||||||
<MainLayout page={<HomePage user={user} />}>
|
|
||||||
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
|
||||||
Khollisé - {user.className} ⚔️
|
|
||||||
</h1>
|
|
||||||
<SyncButton />
|
|
||||||
</MainLayout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { forceReload } from "~/lib/utils";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, isLoading, error } = useUser();
|
const { user, isLoading, error } = useUser();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
@ -20,10 +20,14 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout page={<SettingsPage user={user} />}>
|
<MainLayout
|
||||||
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
header={
|
||||||
Khollisé - {user.className} ⚔️
|
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||||
</h1>
|
Khollisé - {user.className} ⚔️
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPage user={user} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue