{[
t('common.FILTER_TODAY'),
diff --git a/apps/web/app/api/timer/timesheet/bulk-delete/route.ts b/apps/web/app/api/timer/timesheet/bulk-delete/route.ts
new file mode 100644
index 000000000..517e1f624
--- /dev/null
+++ b/apps/web/app/api/timer/timesheet/bulk-delete/route.ts
@@ -0,0 +1,42 @@
+import { deleteTaskTimesheetRequest } from '@/app/services/server/requests';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import { NextResponse } from "next/server";
+
+export async function DELETE(req: Request) {
+ const res = new NextResponse();
+ const body = await req.json();
+ const { logIds = [] } = body;
+
+ if (!Array.isArray(logIds) || logIds.length === 0) {
+ return NextResponse.json(
+ { error: 'logIds must be a non-empty array' },
+ { status: 400 }
+ );
+ }
+
+ const { $res, user, tenantId, organizationId, access_token, } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+ try {
+ const { data } = await deleteTaskTimesheetRequest({
+ tenantId,
+ organizationId,
+ logIds,
+ }, access_token);
+
+ if (!data) {
+ return NextResponse.json(
+ { error: 'No data found' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error('Error delete timesheet:', error);
+ return NextResponse.json(
+ { error: 'Failed to delete timesheet data' },
+ { status: 500 }
+ );
+ }
+
+}
diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts
index db8321a56..b7d0ad636 100644
--- a/apps/web/app/constants.ts
+++ b/apps/web/app/constants.ts
@@ -209,6 +209,8 @@ export const TaskStatus = {
INPROGRESS: 'in-progress'
};
+export const tasksStatusSvgCacheDuration = 1000 * 60 * 60;
+
export const languagesFlags = [
{
Flag: US,
@@ -317,6 +319,8 @@ export const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET;
export const TWITTER_CLIENT_ID = process.env.TWITTER_CLIENT_ID;
export const TWITTER_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET;
+export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true';
+
// Add manual timer reason
export const manualTimeReasons: ManualTimeReasons[] = [
diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
index 60733481f..ca39217a3 100644
--- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts
+++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
@@ -1,16 +1,24 @@
-import { timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores';
+import { timesheetDeleteState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores';
import { useAtom } from 'jotai';
+import React from 'react';
export function useTimelogFilterOptions() {
const [employeeState, setEmployeeState] = useAtom(timesheetFilterEmployeeState);
const [projectState, setProjectState] = useAtom(timesheetFilterProjectState);
const [statusState, setStatusState] = useAtom(timesheetFilterStatusState);
const [taskState, setTaskState] = useAtom(timesheetFilterTaskState);
+ const [selectTimesheet, setSelectTimesheet] = useAtom(timesheetDeleteState);
const employee = employeeState;
const project = projectState;
const task = taskState
+ const handleSelectRowTimesheet = (items: string) => {
+ setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items])
+ }
+ React.useEffect(() => {
+ return () => setSelectTimesheet([]);
+ }, []);
return {
statusState,
@@ -20,6 +28,9 @@ export function useTimelogFilterOptions() {
setEmployeeState,
setProjectState,
setTaskState,
- setStatusState
+ setStatusState,
+ handleSelectRowTimesheet,
+ selectTimesheet,
+ setSelectTimesheet
};
}
diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts
index c11e196ff..3352f9861 100644
--- a/apps/web/app/hooks/features/useTimesheet.ts
+++ b/apps/web/app/hooks/features/useTimesheet.ts
@@ -3,9 +3,9 @@ import { useAtom } from 'jotai';
import { timesheetRapportState } from '@/app/stores/time-logs';
import { useQuery } from '../useQuery';
import { useCallback, useEffect } from 'react';
-import { getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log';
+import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log';
import moment from 'moment';
-import { ITimeSheet } from '@/app/interfaces';
+import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { useTimelogFilterOptions } from './useTimelogFilterOptions';
interface TimesheetParams {
@@ -15,21 +15,35 @@ interface TimesheetParams {
export interface GroupedTimesheet {
date: string;
- tasks: ITimeSheet[];
+ tasks: TimesheetLog[];
}
-const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => {
+interface DeleteTimesheetParams {
+ organizationId: string;
+ tenantId: string;
+ logIds: string[];
+}
+
+
+const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => {
if (!items?.length) return [];
- type GroupedMap = Record
;
+ type GroupedMap = Record;
+
const groupedByDate = items.reduce((acc, item) => {
- if (!item?.createdAt) return acc;
+ if (!item?.timesheet?.createdAt) {
+ console.warn('Skipping item with missing timesheet or createdAt:', item);
+ return acc;
+ }
try {
- const date = new Date(item.createdAt).toISOString().split('T')[0];
+ const date = new Date(item.timesheet.createdAt).toISOString().split('T')[0];
if (!acc[date]) acc[date] = [];
acc[date].push(item);
} catch (error) {
- console.error('Invalid date format:', item.createdAt);
+ console.error(
+ `Failed to process date for timesheet ${item.timesheet.id}:`,
+ { createdAt: item.timesheet.createdAt, error }
+ );
}
return acc;
}, {});
@@ -46,8 +60,10 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
- const { employee, project } = useTimelogFilterOptions();
+ const { employee, project, selectTimesheet: logIds } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
+ const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi)
+
const getTaskTimesheet = useCallback(
({ startDate, endDate }: TimesheetParams) => {
@@ -76,6 +92,70 @@ export function useTimesheet({
project
]
);
+
+ const getStatusTimesheet = (items: TimesheetLog[] = []) => {
+ const STATUS_MAP: Record = {
+ PENDING: [],
+ APPROVED: [],
+ DENIED: [],
+ DRAFT: [],
+ 'IN REVIEW': []
+ };
+
+ return items.reduce((acc, item) => {
+ const status = item.timesheet.status;
+ if (isTimesheetStatus(status)) {
+ acc[status].push(item);
+ } else {
+ console.warn(`Invalid timesheet status: ${status}`);
+ }
+ return acc;
+ }, STATUS_MAP);
+ }
+
+ // Type guard
+ function isTimesheetStatus(status: unknown): status is TimesheetStatus {
+ const timesheetStatusValues: TimesheetStatus[] = [
+ "DRAFT",
+ "PENDING",
+ "IN REVIEW",
+ "DENIED",
+ "APPROVED"
+ ];
+ return Object.values(timesheetStatusValues).includes(status as TimesheetStatus);
+ }
+
+
+ const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => {
+ try {
+ return await queryDeleteTimesheet(params);
+ } catch (error) {
+ console.error('Error deleting timesheet:', error);
+ throw error;
+ }
+ };
+
+ const deleteTaskTimesheet = useCallback(async () => {
+ if (!user) {
+ throw new Error('User not authenticated');
+ }
+ if (!logIds.length) {
+ throw new Error('No timesheet IDs provided for deletion');
+ }
+ try {
+ await handleDeleteTimesheet({
+ organizationId: user.employee.organizationId,
+ tenantId: user.tenantId ?? "",
+ logIds
+ });
+ } catch (error) {
+ console.error('Failed to delete timesheets:', error);
+ throw error;
+ }
+ },
+ [user, queryDeleteTimesheet, logIds, handleDeleteTimesheet] // deepscan-disable-line
+ );
+
useEffect(() => {
getTaskTimesheet({ startDate, endDate });
}, [getTaskTimesheet, startDate, endDate]);
@@ -86,5 +166,8 @@ export function useTimesheet({
loadingTimesheet,
timesheet: groupByDate(timesheet),
getTaskTimesheet,
+ loadingDeleteTimesheet,
+ deleteTaskTimesheet,
+ getStatusTimesheet
};
}
diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts
index 8eba961df..e296af463 100644
--- a/apps/web/app/interfaces/IDailyPlan.ts
+++ b/apps/web/app/interfaces/IDailyPlan.ts
@@ -25,12 +25,12 @@ export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee,
export interface IUpdateDailyPlan
extends Partial,
- Pick,
- Partial> {}
+ Pick,
+ Partial> { }
export interface IDailyPlanTasksUpdate
extends Pick,
- IBasePerTenantAndOrganizationEntity {}
+ IBasePerTenantAndOrganizationEntity { }
export enum DailyPlanStatusEnum {
OPEN = 'open',
@@ -38,4 +38,4 @@ export enum DailyPlanStatusEnum {
COMPLETED = 'completed'
}
-export type IDailyPlanMode = 'today' | 'tomorow' | 'custom';
+export type IDailyPlanMode = 'today' | 'tomorrow' | 'custom';
diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts
index daa407299..0bb4459b3 100644
--- a/apps/web/app/interfaces/ITask.ts
+++ b/apps/web/app/interfaces/ITask.ts
@@ -137,6 +137,14 @@ export type ITaskStatusField =
| 'tags'
| 'status type';
+export type TimesheetStatus =
+ | "DRAFT"
+ | "PENDING"
+ | "IN REVIEW"
+ | "DENIED"
+ | "APPROVED";
+
+
export type ITaskStatusStack = {
status: ITaskStatus;
size: ITaskSize;
diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts
index 3e30da712..d6e469d25 100644
--- a/apps/web/app/interfaces/timer/ITimerLog.ts
+++ b/apps/web/app/interfaces/timer/ITimerLog.ts
@@ -1,66 +1,99 @@
-import { ITaskIssue } from "..";
+import { ITeamTask } from "../ITask";
-interface Project {
+interface BaseEntity {
id: string;
- name: string;
- imageUrl: string;
- membersCount: number;
- image: string | null;
-}
-
-interface Task {
- id: string;
- title: string;
- issueType?: ITaskIssue | null;
- estimate: number | null;
- taskStatus: string | null;
- taskNumber: string;
+ isActive: boolean;
+ isArchived: boolean;
+ tenantId: string;
+ organizationId: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ archivedAt: string | null;
}
-interface OrganizationContact {
- id: string;
- name: string;
- imageUrl: string;
+interface ImageEntity {
+ imageUrl: string | null;
image: string | null;
}
-interface User {
- id: string;
+interface User extends BaseEntity {
firstName: string;
lastName: string;
- imageUrl: string;
- image: string | null;
name: string;
+ imageUrl: string | null;
+ image: string | null;
}
-interface Employee {
- id: string;
+interface Employee extends BaseEntity {
isOnline: boolean;
isAway: boolean;
user: User;
fullName: string;
}
-export interface ITimeSheet {
- deletedAt: string | null;
- id: string;
- createdAt: string;
- updatedAt: string;
- isActive: boolean;
- isArchived: boolean;
- archivedAt: string | null;
- tenantId: string;
- organizationId: string;
+interface TaskStatus extends BaseEntity {
+ name: string;
+ value: string;
+ description: string;
+ order: number;
+ icon: string;
+ color: string;
+ isSystem: boolean;
+ isCollapsed: boolean;
+ isDefault: boolean;
+ isTodo: boolean;
+ isInProgress: boolean;
+ isDone: boolean;
+ projectId: string | null;
+ organizationTeamId: string | null;
+ fullIconUrl: string;
+}
+interface Task extends ITeamTask {
+ taskStatus: TaskStatus | null,
+ number: number;
+ description: string;
+ startDate: string | null;
+}
+
+
+interface Timesheet extends BaseEntity {
+ duration: number;
+ keyboard: number;
+ mouse: number;
+ overall: number;
+ startedAt: string;
+ stoppedAt: string;
+ approvedAt: string | null;
+ submittedAt: string | null;
+ lockedAt: string | null;
+ editedAt: string | null;
+ isBilled: boolean;
+ status: string;
+ employeeId: string;
+ approvedById: string | null;
+ isEdited: boolean;
+}
+interface Project extends BaseEntity, ImageEntity {
+ name: string;
+ membersCount: number;
+}
+
+interface OrganizationContact extends BaseEntity, ImageEntity {
+ name: string;
+}
+
+export interface TimesheetLog extends BaseEntity {
startedAt: string;
stoppedAt: string;
editedAt: string | null;
- logType: string;
- source: string;
+ logType: "TRACKED" | "MANUAL";
+ source: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP";
description: string;
reason: string | null;
isBillable: boolean;
isRunning: boolean;
- version: number | null;
+ version: string | null;
employeeId: string;
timesheetId: string;
projectId: string;
@@ -71,6 +104,7 @@ export interface ITimeSheet {
task: Task;
organizationContact: OrganizationContact;
employee: Employee;
+ timesheet: Timesheet,
duration: number;
isEdited: boolean;
}
diff --git a/apps/web/app/services/client/api/timer/timer-log.ts b/apps/web/app/services/client/api/timer/timer-log.ts
index f3a5b13b4..b35b7abea 100644
--- a/apps/web/app/services/client/api/timer/timer-log.ts
+++ b/apps/web/app/services/client/api/timer/timer-log.ts
@@ -1,5 +1,5 @@
-import { ITimeSheet, ITimerStatus } from '@app/interfaces';
-import { get } from '../../axios';
+import { TimesheetLog, ITimerStatus } from '@app/interfaces';
+import { get, deleteApi } from '../../axios';
export async function getTimerLogs(
tenantId: string,
@@ -53,7 +53,9 @@ export async function getTaskTimesheetLogsApi({
'relations[1]': 'task',
'relations[2]': 'organizationContact',
'relations[3]': 'employee.user',
- 'relations[4]': 'task.taskStatus'
+ 'relations[4]': 'task.taskStatus',
+ 'relations[5]': 'timesheet'
+
});
projectIds.forEach((id, index) => {
@@ -64,5 +66,44 @@ export async function getTaskTimesheetLogsApi({
params.append(`employeeIds[${index}]`, id);
});
const endpoint = `/timesheet/time-log?${params.toString()}`;
- return get(endpoint, { tenantId });
+ return get(endpoint, { tenantId });
+}
+
+
+export async function deleteTaskTimesheetLogsApi({
+ logIds,
+ organizationId,
+ tenantId
+}: {
+ organizationId: string,
+ tenantId: string,
+ logIds: string[]
+}) {
+ // Validate required parameters
+ if (!organizationId || !tenantId || !logIds?.length) {
+ throw new Error('Required parameters missing: organizationId, tenantId, and logIds are required');
+ }
+
+ // Limit bulk deletion size for safety
+ if (logIds.length > 100) {
+ throw new Error('Maximum 100 logs can be deleted at once');
+ }
+
+ const params = new URLSearchParams({
+ organizationId,
+ tenantId
+ });
+ logIds.forEach((id, index) => {
+ if (!id) {
+ throw new Error(`Invalid logId at index ${index}`);
+ }
+ params.append(`logIds[${index}]`, id);
+ });
+
+ const endPoint = `/timesheet/time-log?${params.toString()}`;
+ try {
+ return await deleteApi<{ success: boolean; message: string }>(endPoint, { tenantId });
+ } catch (error) {
+ throw new Error(`Failed to delete timesheet logs`);
+ }
}
diff --git a/apps/web/app/services/server/fetch.ts b/apps/web/app/services/server/fetch.ts
index 32d56cb05..f026969ed 100644
--- a/apps/web/app/services/server/fetch.ts
+++ b/apps/web/app/services/server/fetch.ts
@@ -1,4 +1,4 @@
-import { GAUZY_API_SERVER_URL } from '@app/constants';
+import { GAUZY_API_SERVER_URL, tasksStatusSvgCacheDuration } from '@app/constants';
export function serverFetch({
path,
@@ -54,3 +54,40 @@ export function serverFetch({
};
});
}
+
+/** Tasks status SVG icons fetch */
+
+// In memory cache for performance
+
+const tasksStatusSvgCache = new Map<
+ string,
+ {
+ content: Response;
+ timestamp: number;
+ }
+>();
+
+export async function svgFetch(url: string): Promise {
+ try {
+ //Url validation
+ new URL(url);
+
+ const cached = tasksStatusSvgCache.get(url);
+ const now = Date.now();
+
+ if (cached && now - cached.timestamp < tasksStatusSvgCacheDuration) {
+ return cached.content.clone();
+ }
+
+ // Fetch the SVG
+ const response = await fetch(url);
+
+ tasksStatusSvgCache.set(url, {
+ content: response.clone(),
+ timestamp: now
+ });
+ return response;
+ } catch {
+ throw new Error('Invalid URL provided');
+ }
+}
diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts
index a7a254880..4040697dd 100644
--- a/apps/web/app/services/server/requests/timesheet.ts
+++ b/apps/web/app/services/server/requests/timesheet.ts
@@ -1,7 +1,7 @@
import { ITasksTimesheet } from '@app/interfaces/ITimer';
import { serverFetch } from '../fetch';
import qs from 'qs';
-import { ITimeSheet } from '@/app/interfaces/timer/ITimerLog';
+import { TimesheetLog } from '@/app/interfaces/timer/ITimerLog';
export type TTasksTimesheetStatisticsParams = {
tenantId: string;
@@ -72,10 +72,26 @@ type ITimesheetProps = {
export function getTaskTimesheetRequest(params: ITimesheetProps, bearer_token: string) {
const queries = qs.stringify(params);
- return serverFetch({
+ return serverFetch({
path: `/timesheet/time-log?activityLevel?${queries.toString()}`,
method: 'GET',
bearer_token,
tenantId: params.tenantId
})
}
+
+type IDeleteTimesheetProps = {
+ organizationId: string;
+ tenantId: string;
+ logIds?: string[]
+}
+
+export function deleteTaskTimesheetRequest(params: IDeleteTimesheetProps, bearer_token: string) {
+ const { logIds = [] } = params;
+ return serverFetch({
+ path: `/timesheet/time-log/${logIds.join(',')}`,
+ method: 'DELETE',
+ bearer_token,
+ tenantId: params.tenantId
+ });
+}
diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts
index 9f607b4ff..d015ab9bf 100644
--- a/apps/web/app/stores/time-logs.ts
+++ b/apps/web/app/stores/time-logs.ts
@@ -1,6 +1,6 @@
import { ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs';
import { atom } from 'jotai';
-import { IProject, ITeamTask, ITimeSheet, OT_Member } from '../interfaces';
+import { IProject, ITeamTask, OT_Member, TimesheetLog } from '../interfaces';
interface IFilterOption {
value: string;
@@ -9,10 +9,11 @@ interface IFilterOption {
export const timerLogsDailyReportState = atom([]);
-export const timesheetRapportState = atom([])
+export const timesheetRapportState = atom([])
export const timesheetFilterEmployeeState = atom([]);
export const timesheetFilterProjectState = atom([]);
export const timesheetFilterTaskState = atom([]);
export const timesheetFilterStatusState = atom([]);
+export const timesheetDeleteState = atom([])
diff --git a/apps/web/auth.ts b/apps/web/auth.ts
index 56e936665..36430b60a 100644
--- a/apps/web/auth.ts
+++ b/apps/web/auth.ts
@@ -2,9 +2,11 @@ import NextAuth from 'next-auth';
import { filteredProviders } from '@app/utils/check-provider-env-vars';
import { GauzyAdapter, jwtCallback, ProviderEnum, signInCallback } from '@app/services/server/requests/OAuth';
import { NextRequest } from 'next/server';
+import { IS_DESKTOP_APP } from '@app/constants';
export const { handlers, signIn, signOut, auth } = NextAuth((request) => ({
providers: filteredProviders,
+ trustHost: IS_DESKTOP_APP,
adapter: GauzyAdapter(request as NextRequest),
session: { strategy: 'jwt' },
callbacks: {
diff --git a/apps/web/components/pages/kanban/menu-kanban-card.tsx b/apps/web/components/pages/kanban/menu-kanban-card.tsx
index 457433d6b..26ebbc833 100644
--- a/apps/web/components/pages/kanban/menu-kanban-card.tsx
+++ b/apps/web/components/pages/kanban/menu-kanban-card.tsx
@@ -170,7 +170,7 @@ export default function MenuKanbanCard({ item: task, member }: { item: ITeamTask
-
+
@@ -227,8 +227,7 @@ function TeamMembersSelect(props: ITeamMemberSelectProps): JSX.Element {
- `relative cursor-default select-none py-2 pl-10 pr-4 ${
- active ? 'bg-primary/5' : 'text-gray-900'
+ `relative cursor-default select-none py-2 pl-10 pr-4 ${active ? 'bg-primary/5' : 'text-gray-900'
}`
}
value={member}
diff --git a/apps/web/components/ui/alert-dialog.tsx b/apps/web/components/ui/alert-dialog.tsx
new file mode 100755
index 000000000..61b539897
--- /dev/null
+++ b/apps/web/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "lib/utils"
+import { buttonVariants } from "components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx
index 90859cca5..670456047 100644
--- a/apps/web/components/ui/button.tsx
+++ b/apps/web/components/ui/button.tsx
@@ -1,51 +1,56 @@
-import { Slot } from '@radix-ui/react-slot';
-import { cva, type VariantProps } from 'class-variance-authority';
-import * as React from 'react';
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from 'lib/utils';
+import { cn } from "lib/utils"
const buttonVariants = cva(
- 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline'
- },
- size: {
- default: 'h-10 px-4 py-2',
- sm: 'h-9 rounded-md px-3',
- lg: 'h-11 rounded-md px-8',
- icon: 'h-10 w-10'
- }
- },
- defaultVariants: {
- variant: 'default',
- size: 'default'
- }
- }
-);
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean;
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
}
const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }: ButtonProps, ref) => {
- const Comp = asChild ? Slot : 'button';
- return (
-
- {props.children as React.ReactNode}
-
- );
- }
-);
-Button.displayName = 'Button';
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
-export { Button, buttonVariants };
+export { Button, buttonVariants }
diff --git a/apps/web/components/ui/data-table.tsx b/apps/web/components/ui/data-table.tsx
index 09c74e340..8a9ceaf8e 100644
--- a/apps/web/components/ui/data-table.tsx
+++ b/apps/web/components/ui/data-table.tsx
@@ -86,9 +86,9 @@ function DataTable({ columns, data, footerRows, isHeader }: DataT
{header.isPlaceholder
? null
: flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
+ header.column.columnDef.header,
+ header.getContext()
+ )}
diff --git a/apps/web/lib/components/alert-dialog-confirmation.tsx b/apps/web/lib/components/alert-dialog-confirmation.tsx
new file mode 100644
index 000000000..494a40f12
--- /dev/null
+++ b/apps/web/lib/components/alert-dialog-confirmation.tsx
@@ -0,0 +1,72 @@
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@components/ui/alert-dialog"
+import { ReloadIcon } from "@radix-ui/react-icons";
+import React from "react";
+
+
+
+interface AlertDialogConfirmationProps {
+ title: string;
+ description: string;
+ confirmText?: string;
+ cancelText?: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ loading?: boolean
+}
+
+export function AlertDialogConfirmation({
+ title,
+ description,
+ confirmText = "Continue",
+ cancelText = "Cancel",
+ onConfirm,
+ onCancel,
+ isOpen,
+ onOpenChange,
+ loading
+}: AlertDialogConfirmationProps) {
+ return (
+