So its like broken but we are rewriting status.ts & TechTable & HistoryTable
This commit is contained in:
16
package.json
16
package.json
@ -20,11 +20,14 @@
|
|||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@sentry/nextjs": "^9.28.0",
|
"@sentry/nextjs": "^9.28.1",
|
||||||
"@supabase/ssr": "^0.6.1",
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "^2.50.0",
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
@ -39,26 +42,27 @@
|
|||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.57.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"zod": "^3.25.58"
|
"vaul": "^1.1.2",
|
||||||
|
"zod": "^3.25.63"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"@types/react": "^19.1.7",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-next": "^15.3.3",
|
"eslint-config-next": "^15.3.3",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^5.4.1",
|
"eslint-plugin-prettier": "^5.4.1",
|
||||||
"import-in-the-middle": "^1.14.0",
|
"import-in-the-middle": "^1.14.0",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.8",
|
"tailwindcss": "^4.1.10",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
1064
pnpm-lock.yaml
generated
1064
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -406,11 +406,11 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
|||||||
selfHosted={true}
|
selfHosted={true}
|
||||||
>
|
>
|
||||||
<TVModeProvider>
|
<TVModeProvider>
|
||||||
<div className='min-h-screen'>
|
<main className='min-h-screen'>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</TVModeProvider>
|
</TVModeProvider>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { getUser } from '@/lib/actions';
|
|
||||||
import type { User } from '@/utils/supabase';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
import { getUser } from '@/lib/actions';
|
||||||
|
|
||||||
const HomePage = async () => {
|
const Home = async () => {
|
||||||
const response = await getUser();
|
const userResponse = await getUser();
|
||||||
if (!response.success || !response.data) {
|
if (!userResponse.success) {
|
||||||
redirect('/sign-in');
|
redirect('/sign-in');
|
||||||
|
} else if (userResponse.data) {
|
||||||
|
redirect('/status');
|
||||||
|
} else return <div/>;
|
||||||
}
|
}
|
||||||
const user: User = response.data;
|
export default Home;
|
||||||
return (
|
|
||||||
<div>
|
//'use client';
|
||||||
<h1>Hello {user.email}</h1>
|
|
||||||
</div>
|
////import { TechTable } from '@/components/status';
|
||||||
)
|
//import { redirect } from 'next/navigation';
|
||||||
};
|
//import { useAuth } from '@/components/context';
|
||||||
export default HomePage;
|
|
||||||
|
//const HomePage = () => {
|
||||||
|
//const { isAuthenticated } = useAuth();
|
||||||
|
//if (!isAuthenticated) {
|
||||||
|
//redirect('/sign-in');
|
||||||
|
//}
|
||||||
|
//redirect('/profile');
|
||||||
|
//};
|
||||||
|
//export default HomePage;
|
||||||
|
15
src/app/status/page.tsx
Normal file
15
src/app/status/page.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TechTable } from '@/components/status';
|
||||||
|
import { useAuth } from '@/components/context';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
const Status = () => {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
redirect('/sign-in');
|
||||||
|
} else {
|
||||||
|
return <TechTable />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default Status;
|
236
src/components/status/History_Drawer.tsx
Normal file
236
src/components/status/History_Drawer.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import {
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
ScrollArea,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Button,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import {
|
||||||
|
getUserHistory,
|
||||||
|
getAllHistory,
|
||||||
|
type HistoryEntry,
|
||||||
|
} from '@/lib/hooks';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type HistoryDrawerProps = {
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||||
|
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [totalPages, setTotalPages] = useState<number>(1);
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const perPage = 50;
|
||||||
|
|
||||||
|
const fetchHistory = useCallback(async (currentPage: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (user_id && user_id !== '') {
|
||||||
|
response = await getUserHistory(user_id, currentPage, perPage);
|
||||||
|
} else {
|
||||||
|
response = await getAllHistory(currentPage, perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory(response.data.data);
|
||||||
|
setTotalPages(response.data.meta.total_pages);
|
||||||
|
setTotalCount(response.data.meta.total_count);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(`Error fetching history: ${errorMessage}`);
|
||||||
|
setHistory([]);
|
||||||
|
setTotalPages(1);
|
||||||
|
setTotalCount(0);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [user_id, perPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchHistory(page);
|
||||||
|
}, [page, fetchHistory]);
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage >= 1 && newPage <= totalPages) {
|
||||||
|
setPage(newPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayName = (entry: HistoryEntry) => {
|
||||||
|
return entry.user_profile?.full_name || 'Unknown User';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatedByName = (entry: HistoryEntry) => {
|
||||||
|
if (!entry.updated_by) return 'Self';
|
||||||
|
return entry.updated_by.full_name || 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerContent className='max-w-4xl mx-auto'>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>
|
||||||
|
<div className='flex flex-row items-center justify-center py-4'>
|
||||||
|
<Image
|
||||||
|
src='/favicon.png'
|
||||||
|
alt='Tech Tracker Logo'
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
className='w-8 h-8 md:w-12 md:h-12'
|
||||||
|
/>
|
||||||
|
<h1 className='text-lg md:text-2xl lg:text-4xl font-bold pl-2 md:pl-4'>
|
||||||
|
{user_id && user_id !== '' ? 'User History' : 'All History'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{totalCount > 0 && (
|
||||||
|
<p className='text-sm text-muted-foreground text-center'>
|
||||||
|
{totalCount} total entries
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
|
<div className='px-4'>
|
||||||
|
<ScrollArea className='h-96 w-full'>
|
||||||
|
{loading ? (
|
||||||
|
<div className='flex justify-center items-center h-32'>
|
||||||
|
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary'></div>
|
||||||
|
</div>
|
||||||
|
) : history.length === 0 ? (
|
||||||
|
<div className='flex justify-center items-center h-32'>
|
||||||
|
<p className='text-muted-foreground'>No history found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className='font-semibold'>Name</TableHead>
|
||||||
|
<TableHead className='font-semibold'>Status</TableHead>
|
||||||
|
<TableHead className='font-semibold'>Updated By</TableHead>
|
||||||
|
<TableHead className='font-semibold text-right'>Date & Time</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{history.map((entry) => (
|
||||||
|
<TableRow key={entry.id}>
|
||||||
|
<TableCell className='font-medium'>
|
||||||
|
{getDisplayName(entry)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='max-w-xs'>
|
||||||
|
<div className='truncate' title={entry.status}>
|
||||||
|
{entry.status}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-sm text-muted-foreground'>
|
||||||
|
{getUpdatedByName(entry)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right text-sm'>
|
||||||
|
{formatTime(entry.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DrawerFooter>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
{page > 1 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
href='#'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(page - 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages > 10 ? (
|
||||||
|
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
||||||
|
<span>Page</span>
|
||||||
|
<span className='font-bold text-foreground'>{page}</span>
|
||||||
|
<span>of</span>
|
||||||
|
<span className='font-semibold text-foreground'>{totalPages}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
Array.from({ length: totalPages }).map((_, idx) => (
|
||||||
|
<PaginationItem key={idx}>
|
||||||
|
<PaginationLink
|
||||||
|
href='#'
|
||||||
|
isActive={page === idx + 1}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(idx + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{idx + 1}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{page < totalPages && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href='#'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageChange(page + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button variant='outline' className='mt-4'>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
);
|
||||||
|
};
|
333
src/components/status/Tech_Table.tsx
Normal file
333
src/components/status/Tech_Table.tsx
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useAuth, useTVMode } from '@/components/context';
|
||||||
|
import {
|
||||||
|
getUserStatuses,
|
||||||
|
updateUserStatus,
|
||||||
|
updateCurrentUserStatus,
|
||||||
|
type UserStatus,
|
||||||
|
} from '@/lib/hooks';
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerTrigger,
|
||||||
|
Progress,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { HistoryDrawer } from '@/components/status';
|
||||||
|
import { createClient } from '@/utils/supabase';
|
||||||
|
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
export const TechTable = () => {
|
||||||
|
const { isAuthenticated, profile } = useAuth();
|
||||||
|
const { tvMode } = useTVMode();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
const [statusInput, setStatusInput] = useState('');
|
||||||
|
const [technicianData, setTechnicianData] = useState<UserStatus[]>([]);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState('');
|
||||||
|
const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
const fetchTechnicians = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUserStatuses();
|
||||||
|
if (!response.success) throw new Error(response.error);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(`Error fetching technicians: ${errorMessage}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Setup realtime broadcast subscription
|
||||||
|
const setupRealtimeSubscription = useCallback(() => {
|
||||||
|
console.log('Setting up realtime broadcast subscription');
|
||||||
|
|
||||||
|
const channel = supabase.channel('status_updates');
|
||||||
|
|
||||||
|
channel
|
||||||
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
||||||
|
console.log('Status update received:', payload);
|
||||||
|
|
||||||
|
const userStatus = payload.payload.user_status as UserStatus;
|
||||||
|
|
||||||
|
// Update the technician data
|
||||||
|
setTechnicianData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
const existingIndex = newData.findIndex(tech => tech.id === userStatus.id);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Update existing user if this status is more recent
|
||||||
|
if (new Date(userStatus.created_at) > new Date(newData[existingIndex].created_at)) {
|
||||||
|
newData[existingIndex] = userStatus;
|
||||||
|
|
||||||
|
// Mark as recently updated
|
||||||
|
setRecentlyUpdatedIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(userStatus.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove highlight after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setRecentlyUpdatedIds(current => {
|
||||||
|
const updatedSet = new Set(current);
|
||||||
|
updatedSet.delete(userStatus.id);
|
||||||
|
return updatedSet;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new user
|
||||||
|
newData.push(userStatus);
|
||||||
|
|
||||||
|
// Mark as recently updated
|
||||||
|
setRecentlyUpdatedIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(userStatus.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove highlight after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setRecentlyUpdatedIds(current => {
|
||||||
|
const updatedSet = new Set(current);
|
||||||
|
updatedSet.delete(userStatus.id);
|
||||||
|
return updatedSet;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by most recent
|
||||||
|
newData.sort((a, b) =>
|
||||||
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe((status) => {
|
||||||
|
console.log('Realtime subscription status:', status);
|
||||||
|
});
|
||||||
|
|
||||||
|
channelRef.current = channel;
|
||||||
|
return channel;
|
||||||
|
}, [supabase]);
|
||||||
|
|
||||||
|
const updateStatus = useCallback(async () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
toast.error('You must be signed in to update status.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!statusInput.trim()) {
|
||||||
|
toast.error('Please enter a status.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
// Update current user - find them by profile match
|
||||||
|
let targetUserId = null;
|
||||||
|
if (profile?.full_name) {
|
||||||
|
const currentUserInTable = technicianData.find(tech =>
|
||||||
|
tech.full_name === profile.full_name || tech.id === profile.id
|
||||||
|
);
|
||||||
|
targetUserId = currentUserInTable?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUserId) {
|
||||||
|
const result = await updateUserStatus([targetUserId], statusInput);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
toast.success('Your status has been updated.');
|
||||||
|
} else {
|
||||||
|
const result = await updateCurrentUserStatus(statusInput);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
toast.success('Your status has been updated.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await updateUserStatus(selectedIds, statusInput);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
toast.success(`Status updated for ${selectedIds.length} technician${selectedIds.length > 1 ? 's' : ''}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedIds([]);
|
||||||
|
setStatusInput('');
|
||||||
|
// No need to manually fetch - broadcast will handle updates
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(`Failed to update status: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, statusInput, selectedIds, technicianData, profile]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (id: string) => {
|
||||||
|
setSelectedIds(prev =>
|
||||||
|
prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAllChange = () => {
|
||||||
|
if (selectAll) {
|
||||||
|
setSelectedIds([]);
|
||||||
|
} else {
|
||||||
|
setSelectedIds(technicianData.map(tech => tech.id));
|
||||||
|
}
|
||||||
|
setSelectAll(!selectAll);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const time = date.toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
});
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.toLocaleString('default', { month: 'long' });
|
||||||
|
return `${time} - ${month} ${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial load and setup realtime subscription
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
const data = await fetchTechnicians();
|
||||||
|
setTechnicianData(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadData();
|
||||||
|
|
||||||
|
// Setup realtime subscription
|
||||||
|
const channel = setupRealtimeSubscription();
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
if (channel) {
|
||||||
|
console.log('Removing broadcast channel');
|
||||||
|
void supabase.removeChannel(channel);
|
||||||
|
channelRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fetchTechnicians, setupRealtimeSubscription, supabase]);
|
||||||
|
|
||||||
|
// Update select all state
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectAll(
|
||||||
|
selectedIds.length === technicianData.length &&
|
||||||
|
technicianData.length > 0
|
||||||
|
);
|
||||||
|
}, [selectedIds.length, technicianData.length]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center min-h-[400px]'>
|
||||||
|
<Progress value={33} className='w-64' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full max-w-7xl mx-auto px-4'>
|
||||||
|
<table className={`w-full text-center border-collapse ${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}>
|
||||||
|
<thead>
|
||||||
|
<tr className='bg-muted'>
|
||||||
|
{!tvMode && (
|
||||||
|
<th className='py-3 px-3 border'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='scale-125 cursor-pointer'
|
||||||
|
checked={selectAll}
|
||||||
|
onChange={handleSelectAllChange}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th className='py-3 px-4 border font-semibold'>Name</th>
|
||||||
|
<th className='py-3 px-4 border font-semibold'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger className='hover:underline'>
|
||||||
|
Status
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer user_id='' />
|
||||||
|
</Drawer>
|
||||||
|
</th>
|
||||||
|
<th className='py-3 px-4 border font-semibold'>Updated At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{technicianData.map((technician, index) => (
|
||||||
|
<tr
|
||||||
|
key={technician.id}
|
||||||
|
className={`
|
||||||
|
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
||||||
|
hover:bg-muted/75 transition-all duration-300
|
||||||
|
${recentlyUpdatedIds.has(technician.id) ? 'ring-2 ring-blue-500 ring-opacity-75 bg-blue-50 dark:bg-blue-900/20' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{!tvMode && (
|
||||||
|
<td className='py-2 px-3 border'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='scale-125 cursor-pointer'
|
||||||
|
checked={selectedIds.includes(technician.id)}
|
||||||
|
onChange={() => handleCheckboxChange(technician.id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className='py-3 px-4 border font-medium'>
|
||||||
|
{technician.full_name ?? 'Unknown User'}
|
||||||
|
</td>
|
||||||
|
<td className='py-3 px-4 border'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger
|
||||||
|
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
|
||||||
|
onClick={() => setSelectedUserId(technician.id)}
|
||||||
|
>
|
||||||
|
{technician.status}
|
||||||
|
</DrawerTrigger>
|
||||||
|
{selectedUserId === technician.id && (
|
||||||
|
<HistoryDrawer
|
||||||
|
key={selectedUserId}
|
||||||
|
user_id={selectedUserId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</td>
|
||||||
|
<td className='py-3 px-4 border text-muted-foreground'>
|
||||||
|
{formatTime(technician.created_at)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{!tvMode && (
|
||||||
|
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type='text'
|
||||||
|
placeholder='New Status'
|
||||||
|
className='min-w-[120px] lg:min-w-[400px] py-2 px-3 rounded-xl border bg-background lg:text-2xl focus:outline-none focus:ring-2 focus:ring-primary'
|
||||||
|
value={statusInput}
|
||||||
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
void updateStatus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-[100px] lg:min-w-[160px] py-2 px-4 rounded-xl text-center font-semibold lg:text-2xl bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
onClick={() => void updateStatus()}
|
||||||
|
disabled={!statusInput.trim()}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
2
src/components/status/index.tsx
Normal file
2
src/components/status/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './HistoryDrawer';
|
||||||
|
export * from './TechTable';
|
135
src/components/ui/drawer.tsx
Normal file
135
src/components/ui/drawer.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
@ -1,11 +1,16 @@
|
|||||||
export * from '@/components/ui/avatar';
|
export * from './avatar';
|
||||||
export * from '@/components/ui/badge';
|
export * from './badge';
|
||||||
export * from '@/components/ui/button';
|
export * from './button';
|
||||||
export * from '@/components/ui/card';
|
export * from './card';
|
||||||
export * from '@/components/ui/checkbox';
|
export * from './checkbox';
|
||||||
export * from '@/components/ui/dropdown-menu';
|
export * from './drawer';
|
||||||
export * from '@/components/ui/form';
|
export * from './dropdown-menu';
|
||||||
export * from '@/components/ui/input';
|
export * from './form';
|
||||||
export * from '@/components/ui/label';
|
export * from './input';
|
||||||
export * from '@/components/ui/separator';
|
export * from './label';
|
||||||
export * from '@/components/ui/sonner';
|
export * from './pagination';
|
||||||
|
export * from './progress';
|
||||||
|
export * from './scroll-area';
|
||||||
|
export * from './separator';
|
||||||
|
export * from './sonner';
|
||||||
|
export * from './table';
|
||||||
|
127
src/components/ui/pagination.tsx
Normal file
127
src/components/ui/pagination.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
}
|
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress }
|
58
src/components/ui/scroll-area.tsx
Normal file
58
src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
116
src/components/ui/table.tsx
Normal file
116
src/components/ui/table.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
142
src/lib/actions/_status.ts
Normal file
142
src/lib/actions/_status.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import 'server-only';
|
||||||
|
import { createServerClient } from '@/utils/supabase';
|
||||||
|
import type { Result } from '.';
|
||||||
|
import type { Profile } from '@/utils/supabase';
|
||||||
|
|
||||||
|
export type UserStatus = Profile & {
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_by?: Profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecentUsers = async (): Promise<Result<string[]>> => {
|
||||||
|
try {
|
||||||
|
const supabase = await createServerClient();
|
||||||
|
|
||||||
|
// Get users who have had status updates in the last week
|
||||||
|
const oneWeekAgo = new Date();
|
||||||
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('user_id')
|
||||||
|
.gte('created_at', oneWeekAgo.toISOString())
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Get unique user IDs
|
||||||
|
const uniqueUserIds = [...new Set(data.map(status => status.user_id))];
|
||||||
|
|
||||||
|
return { success: true, data: uniqueUserIds };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error getting recent users',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserStatuses = async (): Promise<Result<UserStatus[]>> => {
|
||||||
|
try {
|
||||||
|
const supabase = await createServerClient();
|
||||||
|
|
||||||
|
// First get the recent users
|
||||||
|
const recentUsersResult = await getRecentUsers();
|
||||||
|
if (!recentUsersResult.success) {
|
||||||
|
throw new Error(recentUsersResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = recentUsersResult.data;
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
return { success: true, data: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent status for each user
|
||||||
|
const { data: statusData, error: statusError } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('user_id, status, created_at, updated_by_id')
|
||||||
|
.in('user_id', userIds)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (statusError) throw statusError;
|
||||||
|
|
||||||
|
if (!statusData) {
|
||||||
|
return { success: true, data: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by user_id and get the most recent status for each user
|
||||||
|
const userStatusMap = new Map<string, typeof statusData[0]>();
|
||||||
|
|
||||||
|
statusData.forEach(status => {
|
||||||
|
if (!userStatusMap.has(status.user_id)) {
|
||||||
|
userStatusMap.set(status.user_id, status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all unique user IDs from the status data
|
||||||
|
const statusUserIds = Array.from(userStatusMap.keys());
|
||||||
|
|
||||||
|
// Get profile information for these users
|
||||||
|
const { data: profileData, error: profileError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('id, full_name, email, avatar_url, provider, updated_at')
|
||||||
|
.in('id', statusUserIds);
|
||||||
|
|
||||||
|
if (profileError) throw profileError;
|
||||||
|
|
||||||
|
// Get updated_by profile information
|
||||||
|
const updatedByIds = Array.from(userStatusMap.values())
|
||||||
|
.map(status => status.updated_by_id)
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
|
||||||
|
const { data: updatedByData, error: updatedByError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('id, full_name, email, avatar_url, provider, updated_at')
|
||||||
|
.in('id', updatedByIds);
|
||||||
|
|
||||||
|
if (updatedByError) throw updatedByError;
|
||||||
|
|
||||||
|
// Create maps for easy lookup
|
||||||
|
const profileMap = new Map(profileData?.map(profile => [profile.id, profile]) ?? []);
|
||||||
|
const updatedByMap = new Map(updatedByData?.map(profile => [profile.id, profile]) ?? []);
|
||||||
|
|
||||||
|
// Transform the data to match UserStatus type
|
||||||
|
const userStatuses: UserStatus[] = [];
|
||||||
|
|
||||||
|
for (const status of userStatusMap.values()) {
|
||||||
|
const profile = profileMap.get(status.user_id);
|
||||||
|
const updatedBy = status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined;
|
||||||
|
|
||||||
|
if (!profile) continue; // Skip if no profile found
|
||||||
|
|
||||||
|
userStatuses.push({
|
||||||
|
// Profile fields
|
||||||
|
id: profile.id,
|
||||||
|
full_name: profile.full_name,
|
||||||
|
email: profile.email,
|
||||||
|
avatar_url: profile.avatar_url,
|
||||||
|
provider: profile.provider,
|
||||||
|
updated_at: profile.updated_at,
|
||||||
|
// Status fields
|
||||||
|
status: status.status,
|
||||||
|
created_at: status.created_at,
|
||||||
|
updated_by: updatedBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { success: true, data: userStatuses };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error getting user statuses',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './storage';
|
|
||||||
export * from './public';
|
export * from './public';
|
||||||
|
export * from './status';
|
||||||
|
export * from './storage';
|
||||||
|
|
||||||
export type Result<T> =
|
export type Result<T> =
|
||||||
| { success: true; data: T }
|
| { success: true; data: T }
|
||||||
|
53
src/lib/actions/status.ts
Normal file
53
src/lib/actions/status.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import 'server-only';
|
||||||
|
import {
|
||||||
|
createServerClient,
|
||||||
|
type Profile,
|
||||||
|
type Result,
|
||||||
|
type Status,
|
||||||
|
} from '@/utils/supabase';
|
||||||
|
|
||||||
|
type UserWithStatus = {
|
||||||
|
user: Profile;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_by: Profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PaginatedHistory = UserWithStatus[] & {
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersWithStatuses = async () => {
|
||||||
|
try {
|
||||||
|
const supabase = await createServerClient();
|
||||||
|
|
||||||
|
// Get only users with recent statuses (Past 7 days)
|
||||||
|
const oneWeekAgo = new Date();
|
||||||
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
|
|
||||||
|
const { data: recentStatuses, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select(`
|
||||||
|
user:profiles!user_id(*),
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_by:profiles!updated_by_id(*)
|
||||||
|
`)
|
||||||
|
.gte('created_at', oneWeekAgo.toISOString())
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
if (!recentStatuses.length) return { success: true, data: []};
|
||||||
|
|
||||||
|
return { success: true, data: recentStatuses };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `Error: ${error as string}` };
|
||||||
|
}
|
||||||
|
};
|
401
src/lib/hooks/_status.ts
Normal file
401
src/lib/hooks/_status.ts
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
'use client';
|
||||||
|
import { createClient } from '@/utils/supabase';
|
||||||
|
import type { Result } from '.';
|
||||||
|
import type { Profile, Status } from '@/utils/supabase';
|
||||||
|
|
||||||
|
export type UserStatus = Profile & {
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_by?: Profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HistoryEntry = {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_by?: Profile;
|
||||||
|
user_profile: Profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaginatedHistory = {
|
||||||
|
data: HistoryEntry[];
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserStatuses = async (): Promise<Result<UserStatus[]>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
// Get users with recent activity (last 7 days)
|
||||||
|
const oneWeekAgo = new Date();
|
||||||
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
|
|
||||||
|
const { data: recentStatuses, error: statusError } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('*')
|
||||||
|
.gte('created_at', oneWeekAgo.toISOString())
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (statusError) throw statusError;
|
||||||
|
if (!recentStatuses?.length) return { success: true, data: [] };
|
||||||
|
|
||||||
|
// Properly type the status data
|
||||||
|
const typedStatuses: Status[] = recentStatuses;
|
||||||
|
|
||||||
|
// Get most recent status per user
|
||||||
|
const userStatusMap = new Map<string, Status>();
|
||||||
|
typedStatuses.forEach(status => {
|
||||||
|
if (!userStatusMap.has(status.user_id)) {
|
||||||
|
userStatusMap.set(status.user_id, status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const userIds = Array.from(userStatusMap.keys());
|
||||||
|
|
||||||
|
// Get profiles
|
||||||
|
const { data: profiles, error: profileError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.in('id', userIds);
|
||||||
|
|
||||||
|
if (profileError) throw profileError;
|
||||||
|
|
||||||
|
// Get updated_by profiles - filter out nulls properly
|
||||||
|
const updatedByIds = Array.from(userStatusMap.values())
|
||||||
|
.map(s => s.updated_by_id)
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
|
||||||
|
let updatedByProfiles: Profile[] = [];
|
||||||
|
if (updatedByIds.length > 0) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.in('id', updatedByIds);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
updatedByProfiles = data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileMap = new Map((profiles ?? []).map(p => [p.id, p]));
|
||||||
|
const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
const userStatuses: UserStatus[] = [];
|
||||||
|
|
||||||
|
for (const [userId, status] of userStatusMap) {
|
||||||
|
const profile = profileMap.get(userId);
|
||||||
|
if (!profile) continue;
|
||||||
|
|
||||||
|
userStatuses.push({
|
||||||
|
...profile,
|
||||||
|
status: status.status,
|
||||||
|
created_at: status.created_at,
|
||||||
|
updated_by: status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: userStatuses };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const broadcastStatusUpdate = async (
|
||||||
|
userStatus: UserStatus
|
||||||
|
): Promise<Result<void>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
const broadcast = await supabase.channel('status_updates').send({
|
||||||
|
type: 'broadcast',
|
||||||
|
event: 'status_updated',
|
||||||
|
payload: {
|
||||||
|
user_status: userStatus,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (broadcast === 'error') throw new Error('Failed to broadcast status update');
|
||||||
|
if (broadcast === 'ok') return { success: true, data: undefined };
|
||||||
|
else throw new Error('Broadcast timed out!')
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update your existing functions to broadcast after database updates
|
||||||
|
export const updateUserStatus = async (
|
||||||
|
userIds: string[],
|
||||||
|
status: string,
|
||||||
|
): Promise<Result<void>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (!user) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
const inserts = userIds.map(userId => ({
|
||||||
|
user_id: userId,
|
||||||
|
status,
|
||||||
|
updated_by_id: user.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { data: insertedStatuses, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.insert(inserts)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Broadcast the updates
|
||||||
|
if (insertedStatuses) {
|
||||||
|
for (const insertedStatus of insertedStatuses) {
|
||||||
|
// Get the user profile for broadcasting
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', insertedStatus.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
const userStatus: UserStatus = {
|
||||||
|
...profile,
|
||||||
|
status: insertedStatus.status,
|
||||||
|
created_at: insertedStatus.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
await broadcastStatusUpdate(userStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCurrentUserStatus = async (
|
||||||
|
status: string,
|
||||||
|
): Promise<Result<void>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (!user) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
const { data: insertedStatus, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.insert({
|
||||||
|
user_id: user.id,
|
||||||
|
status,
|
||||||
|
updated_by_id: user.id,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Get the user profile for broadcasting
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile && insertedStatus) {
|
||||||
|
const userStatus: UserStatus = {
|
||||||
|
...profile,
|
||||||
|
status: insertedStatus.status,
|
||||||
|
created_at: insertedStatus.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
await broadcastStatusUpdate(userStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserHistory = async (
|
||||||
|
userId: string,
|
||||||
|
page = 1,
|
||||||
|
perPage = 50,
|
||||||
|
): Promise<Result<PaginatedHistory>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const offset = (page - 1) * perPage;
|
||||||
|
|
||||||
|
// Get count
|
||||||
|
const { count } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
// Get data
|
||||||
|
const { data: statuses, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.range(offset, offset + perPage - 1);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const typedStatuses: Status[] = statuses ?? [];
|
||||||
|
|
||||||
|
// Get user profile
|
||||||
|
const { data: userProfile, error: userProfileError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (userProfileError) throw userProfileError;
|
||||||
|
if (!userProfile) throw new Error('User profile not found');
|
||||||
|
|
||||||
|
// Get updated_by profiles - filter out nulls properly
|
||||||
|
const updatedByIds = typedStatuses
|
||||||
|
.map(s => s.updated_by_id)
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
|
||||||
|
let updatedByProfiles: Profile[] = [];
|
||||||
|
if (updatedByIds.length > 0) {
|
||||||
|
const { data, error: updatedByError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.in('id', updatedByIds);
|
||||||
|
|
||||||
|
if (updatedByError) throw updatedByError;
|
||||||
|
updatedByProfiles = data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
const historyEntries: HistoryEntry[] = typedStatuses.map(entry => ({
|
||||||
|
id: entry.id,
|
||||||
|
status: entry.status,
|
||||||
|
created_at: entry.created_at,
|
||||||
|
updated_by: entry.updated_by_id ? updatedByMap.get(entry.updated_by_id) : undefined,
|
||||||
|
user_profile: userProfile,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalCount = count ?? 0;
|
||||||
|
const totalPages = Math.ceil(totalCount / perPage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
data: historyEntries,
|
||||||
|
meta: {
|
||||||
|
current_page: page,
|
||||||
|
per_page: perPage,
|
||||||
|
total_pages: totalPages,
|
||||||
|
total_count: totalCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllHistory = async (
|
||||||
|
page = 1,
|
||||||
|
perPage = 50,
|
||||||
|
): Promise<Result<PaginatedHistory>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const offset = (page - 1) * perPage;
|
||||||
|
|
||||||
|
// Get count
|
||||||
|
const { count } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('*', { count: 'exact', head: true });
|
||||||
|
|
||||||
|
// Get data
|
||||||
|
const { data: statuses, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.range(offset, offset + perPage - 1);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const typedStatuses: Status[] = statuses ?? [];
|
||||||
|
|
||||||
|
// Get all profiles - filter out nulls properly
|
||||||
|
const userIds = [...new Set(typedStatuses.map(s => s.user_id))];
|
||||||
|
const updatedByIds = typedStatuses
|
||||||
|
.map(s => s.updated_by_id)
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
const allIds = [...new Set([...userIds, ...updatedByIds])];
|
||||||
|
|
||||||
|
const { data: profiles, error: profileError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.in('id', allIds);
|
||||||
|
|
||||||
|
if (profileError) throw profileError;
|
||||||
|
|
||||||
|
const profileMap = new Map((profiles ?? []).map(p => [p.id, p]));
|
||||||
|
|
||||||
|
const historyEntries: HistoryEntry[] = typedStatuses.map(entry => {
|
||||||
|
const userProfile = profileMap.get(entry.user_id);
|
||||||
|
if (!userProfile) {
|
||||||
|
throw new Error(`User profile not found for ID: ${entry.user_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
status: entry.status,
|
||||||
|
created_at: entry.created_at,
|
||||||
|
updated_by: entry.updated_by_id ? profileMap.get(entry.updated_by_id) : undefined,
|
||||||
|
user_profile: userProfile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = count ?? 0;
|
||||||
|
const totalPages = Math.ceil(totalCount / perPage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
data: historyEntries,
|
||||||
|
meta: {
|
||||||
|
current_page: page,
|
||||||
|
per_page: perPage,
|
||||||
|
total_pages: totalPages,
|
||||||
|
total_count: totalCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './public';
|
export * from './public';
|
||||||
|
export * from './status';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './useFileUpload';
|
export * from './useFileUpload';
|
||||||
|
|
||||||
|
52
src/lib/hooks/status.ts
Normal file
52
src/lib/hooks/status.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
type Profile,
|
||||||
|
type Result,
|
||||||
|
type Status,
|
||||||
|
} from '@/utils/supabase';
|
||||||
|
|
||||||
|
type UserWithStatus = {
|
||||||
|
user: Profile;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_by: Profile;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PaginatedHistory = UserWithStatus[] & {
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersWithStatuses = async (): Promise<Result<UserWithStatus[]>> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
// Get only users with recent statuses (Past 7 days)
|
||||||
|
const oneWeekAgo = new Date();
|
||||||
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
|
|
||||||
|
const { data: recentStatuses, error } = await supabase
|
||||||
|
.from('statuses')
|
||||||
|
.select(`
|
||||||
|
user:profiles!user_id(*),
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_by:profiles!updated_by_id(*)
|
||||||
|
`)
|
||||||
|
.gte('created_at', oneWeekAgo.toISOString())
|
||||||
|
.order('created_at', { ascending: false }) as {data: UserWithStatus[], error: unknown};
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
if (!recentStatuses.length) return { success: true, data: []};
|
||||||
|
|
||||||
|
return { success: true, data: recentStatuses };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `Error: ${error as string}` };
|
||||||
|
}
|
||||||
|
};
|
@ -64,62 +64,63 @@ create policy "Anyone can update an avatar." on storage.objects
|
|||||||
create policy "Anyone can delete an avatar." on storage.objects
|
create policy "Anyone can delete an avatar." on storage.objects
|
||||||
for delete using (bucket_id = 'avatars');
|
for delete using (bucket_id = 'avatars');
|
||||||
|
|
||||||
-- -- Create a table for public statuses
|
-- Create a table for public statuses
|
||||||
-- CREATE TABLE statuses (
|
CREATE TABLE statuses (
|
||||||
-- id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
-- user_id uuid REFERENCES auth.users ON DELETE CASCADE NOT NULL,
|
user_id uuid REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
|
||||||
-- updated_by_id uuid REFERENCES auth.users ON DELETE SET NULL DEFAULT auth.uid(),
|
updated_by_id uuid REFERENCES public.profiles ON DELETE SET NULL DEFAULT auth.uid(),
|
||||||
-- created_at timestamp with time zone DEFAULT now() NOT NULL,
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
-- status text NOT NULL,
|
status text NOT NULL,
|
||||||
-- CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80)
|
CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80)
|
||||||
-- );
|
);
|
||||||
|
|
||||||
-- -- Set up Row Level Security (RLS)
|
-- Set up Row Level Security (RLS)
|
||||||
-- ALTER TABLE statuses
|
ALTER TABLE statuses
|
||||||
-- ENABLE ROW LEVEL SECURITY;
|
ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- -- Policies
|
-- Policies
|
||||||
-- CREATE POLICY "Public statuses are viewable by everyone." ON statuses
|
CREATE POLICY "Public statuses are viewable by everyone." ON statuses
|
||||||
-- FOR SELECT USING (true);
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
-- -- RECREATE it using the recommended sub-select form
|
-- RECREATE it using the recommended sub-select form
|
||||||
-- CREATE POLICY "Authenticated users can insert statuses for any user."
|
CREATE POLICY "Authenticated users can insert statuses for any user."
|
||||||
-- ON public.statuses
|
ON public.statuses
|
||||||
-- FOR INSERT
|
FOR INSERT
|
||||||
-- WITH CHECK (
|
WITH CHECK (
|
||||||
-- (SELECT auth.role()) = 'authenticated'
|
(SELECT auth.role()) = 'authenticated'
|
||||||
-- );
|
);
|
||||||
|
|
||||||
-- -- ADD an UPDATE policy so anyone signed-in can update *any* status
|
-- ADD an UPDATE policy so anyone signed-in can update *any* status
|
||||||
-- CREATE POLICY "Authenticated users can update statuses for any user."
|
CREATE POLICY "Authenticated users can update statuses for any user."
|
||||||
-- ON public.statuses
|
ON public.statuses
|
||||||
-- FOR UPDATE
|
FOR UPDATE
|
||||||
-- USING (
|
USING (
|
||||||
-- (SELECT auth.role()) = 'authenticated'
|
(SELECT auth.role()) = 'authenticated'
|
||||||
-- )
|
)
|
||||||
-- WITH CHECK (
|
WITH CHECK (
|
||||||
-- (SELECT auth.role()) = 'authenticated'
|
(SELECT auth.role()) = 'authenticated'
|
||||||
-- );
|
);
|
||||||
|
|
||||||
-- -- Function to add first status
|
-- Function to add first status
|
||||||
-- CREATE FUNCTION public.handle_first_status()
|
CREATE FUNCTION public.handle_first_status()
|
||||||
-- RETURNS TRIGGER
|
RETURNS TRIGGER
|
||||||
-- SET search_path = ''
|
SET search_path = ''
|
||||||
-- AS $$
|
AS $$
|
||||||
-- BEGIN
|
BEGIN
|
||||||
-- INSERT INTO public.statuses (user_id, updated_by_id, status)
|
INSERT INTO public.statuses (user_id, updated_by_id, status)
|
||||||
-- VALUES (
|
VALUES (
|
||||||
-- NEW.id,
|
NEW.id,
|
||||||
-- NEW.id,
|
NEW.id,
|
||||||
-- 'Just joined!'
|
'Just joined!'
|
||||||
-- );
|
);
|
||||||
-- RETURN NEW;
|
RETURN NEW;
|
||||||
-- END;
|
END;
|
||||||
-- $$ LANGUAGE plpgsql SECURITY DEFINER;
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
-- -- Create a separate trigger for the status
|
-- Create a separate trigger for the status
|
||||||
-- CREATE TRIGGER on_auth_user_created_add_status
|
CREATE TRIGGER on_auth_user_created_add_status
|
||||||
-- AFTER INSERT ON auth.users
|
AFTER INSERT ON auth.users
|
||||||
-- FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status();
|
FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status();
|
||||||
|
|
||||||
-- alter publication supabase_realtime add table statuses;
|
alter publication supabase_realtime add table profiles;
|
||||||
|
alter publication supabase_realtime add table statuses;
|
||||||
|
@ -4,185 +4,200 @@ export type Json =
|
|||||||
| boolean
|
| boolean
|
||||||
| null
|
| null
|
||||||
| { [key: string]: Json | undefined }
|
| { [key: string]: Json | undefined }
|
||||||
| Json[];
|
| Json[]
|
||||||
|
|
||||||
export type Database = {
|
export type Database = {
|
||||||
public: {
|
public: {
|
||||||
Tables: {
|
Tables: {
|
||||||
profiles: {
|
profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
avatar_url: string | null;
|
avatar_url: string | null
|
||||||
email: string | null;
|
email: string | null
|
||||||
full_name: string | null;
|
full_name: string | null
|
||||||
id: string;
|
id: string
|
||||||
provider: string | null;
|
provider: string | null
|
||||||
updated_at: string | null;
|
updated_at: string | null
|
||||||
};
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null
|
||||||
email?: string | null;
|
email?: string | null
|
||||||
full_name?: string | null;
|
full_name?: string | null
|
||||||
id: string;
|
id: string
|
||||||
provider?: string | null;
|
provider?: string | null
|
||||||
updated_at?: string | null;
|
updated_at?: string | null
|
||||||
};
|
}
|
||||||
Update: {
|
Update: {
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null
|
||||||
email?: string | null;
|
email?: string | null
|
||||||
full_name?: string | null;
|
full_name?: string | null
|
||||||
id?: string;
|
id?: string
|
||||||
provider?: string | null;
|
provider?: string | null
|
||||||
updated_at?: string | null;
|
updated_at?: string | null
|
||||||
};
|
}
|
||||||
Relationships: [];
|
Relationships: []
|
||||||
};
|
}
|
||||||
statuses: {
|
statuses: {
|
||||||
Row: {
|
Row: {
|
||||||
created_at: string;
|
created_at: string
|
||||||
id: string;
|
id: string
|
||||||
status: string;
|
status: string
|
||||||
updated_by_id: string | null;
|
updated_by_id: string | null
|
||||||
user_id: string;
|
user_id: string
|
||||||
};
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
created_at?: string;
|
created_at?: string
|
||||||
id?: string;
|
id?: string
|
||||||
status: string;
|
status: string
|
||||||
updated_by_id?: string | null;
|
updated_by_id?: string | null
|
||||||
user_id: string;
|
user_id: string
|
||||||
};
|
}
|
||||||
Update: {
|
Update: {
|
||||||
created_at?: string;
|
created_at?: string
|
||||||
id?: string;
|
id?: string
|
||||||
status?: string;
|
status?: string
|
||||||
updated_by_id?: string | null;
|
updated_by_id?: string | null
|
||||||
user_id?: string;
|
user_id?: string
|
||||||
};
|
}
|
||||||
Relationships: [];
|
Relationships: [
|
||||||
};
|
{
|
||||||
};
|
foreignKeyName: "statuses_updated_by_id_fkey"
|
||||||
|
columns: ["updated_by_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "profiles"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "statuses_user_id_fkey"
|
||||||
|
columns: ["user_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "profiles"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Views: {
|
Views: {
|
||||||
[_ in never]: never;
|
[_ in never]: never
|
||||||
};
|
}
|
||||||
Functions: {
|
Functions: {
|
||||||
[_ in never]: never;
|
[_ in never]: never
|
||||||
};
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
[_ in never]: never;
|
[_ in never]: never
|
||||||
};
|
}
|
||||||
CompositeTypes: {
|
CompositeTypes: {
|
||||||
[_ in never]: never;
|
[_ in never]: never
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
type DefaultSchema = Database[Extract<keyof Database, 'public'>];
|
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||||
|
|
||||||
export type Tables<
|
export type Tables<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
|
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database;
|
schema: keyof Database
|
||||||
}
|
}
|
||||||
? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
|
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
|
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||||
Row: infer R;
|
Row: infer R
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] &
|
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||||
DefaultSchema['Views'])
|
DefaultSchema["Views"])
|
||||||
? (DefaultSchema['Tables'] &
|
? (DefaultSchema["Tables"] &
|
||||||
DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
|
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||||
Row: infer R;
|
Row: infer R
|
||||||
}
|
}
|
||||||
? R
|
? R
|
||||||
: never
|
: never
|
||||||
: never;
|
: never
|
||||||
|
|
||||||
export type TablesInsert<
|
export type TablesInsert<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof DefaultSchema['Tables']
|
| keyof DefaultSchema["Tables"]
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database;
|
schema: keyof Database
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
Insert: infer I;
|
Insert: infer I
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Insert: infer I;
|
Insert: infer I
|
||||||
}
|
}
|
||||||
? I
|
? I
|
||||||
: never
|
: never
|
||||||
: never;
|
: never
|
||||||
|
|
||||||
export type TablesUpdate<
|
export type TablesUpdate<
|
||||||
DefaultSchemaTableNameOrOptions extends
|
DefaultSchemaTableNameOrOptions extends
|
||||||
| keyof DefaultSchema['Tables']
|
| keyof DefaultSchema["Tables"]
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
schema: keyof Database;
|
schema: keyof Database
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
Update: infer U;
|
Update: infer U
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
: never
|
: never
|
||||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
Update: infer U;
|
Update: infer U
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
: never
|
: never
|
||||||
: never;
|
: never
|
||||||
|
|
||||||
export type Enums<
|
export type Enums<
|
||||||
DefaultSchemaEnumNameOrOptions extends
|
DefaultSchemaEnumNameOrOptions extends
|
||||||
| keyof DefaultSchema['Enums']
|
| keyof DefaultSchema["Enums"]
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||||
schema: keyof Database;
|
schema: keyof Database
|
||||||
}
|
}
|
||||||
? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
|
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
|
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
|
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||||
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
|
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||||
: never;
|
: never
|
||||||
|
|
||||||
export type CompositeTypes<
|
export type CompositeTypes<
|
||||||
PublicCompositeTypeNameOrOptions extends
|
PublicCompositeTypeNameOrOptions extends
|
||||||
| keyof DefaultSchema['CompositeTypes']
|
| keyof DefaultSchema["CompositeTypes"]
|
||||||
| { schema: keyof Database },
|
| { schema: keyof Database },
|
||||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||||
schema: keyof Database;
|
schema: keyof Database
|
||||||
}
|
}
|
||||||
? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
|
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||||
: never = never,
|
: never = never,
|
||||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||||
? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
|
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
|
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||||
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
|
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||||
: never;
|
: never
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
public: {
|
public: {
|
||||||
Enums: {},
|
Enums: {},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import type { Database } from '@/utils/supabase/types';
|
import type { Database } from '@/utils/supabase/types';
|
||||||
export type { User } from '@supabase/supabase-js';
|
export type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
// Result type for API calls
|
||||||
|
export type Result<T> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: string };
|
||||||
|
|
||||||
// Table row types
|
// Table row types
|
||||||
export type Profile = Database['public']['Tables']['profiles']['Row'];
|
export type Profile = Database['public']['Tables']['profiles']['Row'];
|
||||||
export type Status = Database['public']['Tables']['statuses']['Row'];
|
export type Status = Database['public']['Tables']['statuses']['Row'];
|
||||||
|
Reference in New Issue
Block a user