Merge branch 'master' of https://git.shiblysolution.id/TPAY/dashboard
This commit is contained in:
@ -5,11 +5,6 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { DateRangePicker } from '@/pages/dashboards/home/blocks';
|
import { DateRangePicker } from '@/pages/dashboards/home/blocks';
|
||||||
|
|
||||||
const getOneMonthsAgo = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (date: Date): string => date.toISOString().split('T')[0];
|
const formatDate = (date: Date): string => date.toISOString().split('T')[0];
|
||||||
|
|
||||||
const ListToolBar = () => {
|
const ListToolBar = () => {
|
||||||
@ -22,8 +17,8 @@ const ListToolBar = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const threeMonthsAgo = getOneMonthsAgo();
|
const formatted = formatDate(today);
|
||||||
setDateRange({ from: formatDate(threeMonthsAgo), to: formatDate(today) });
|
setDateRange({ from: formatted, to: formatted });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,26 +29,25 @@ const ListToolBar = () => {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchValue, table]);
|
}, [searchValue, table]);
|
||||||
|
|
||||||
const handleFilterByDate = useCallback(() => {
|
|
||||||
try {
|
|
||||||
table.getColumn('created_at')?.setFilterValue(dateRange);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Error applying date filter');
|
|
||||||
console.error('Error applying date filter:', error);
|
|
||||||
}
|
|
||||||
}, [dateRange, table]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dateRange.from && dateRange.to) {
|
const today = new Date();
|
||||||
handleFilterByDate();
|
const formatted = formatDate(today);
|
||||||
|
const initialDateRange = { from: formatted, to: formatted };
|
||||||
|
|
||||||
|
setDateRange(initialDateRange);
|
||||||
|
|
||||||
|
try {
|
||||||
|
table.getColumn('created_at')?.setFilterValue(initialDateRange);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Error applying initial date filter');
|
||||||
|
console.error('Initial date filter error:', error);
|
||||||
}
|
}
|
||||||
}, [dateRange, handleFilterByDate]);
|
}, []);
|
||||||
|
|
||||||
const handleClearAllFilters = () => {
|
const handleClearAllFilters = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const oneMonthAgo = getOneMonthsAgo();
|
|
||||||
const resetDateRange = {
|
const resetDateRange = {
|
||||||
from: formatDate(oneMonthAgo),
|
from: formatDate(today),
|
||||||
to: formatDate(today)
|
to: formatDate(today)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,21 +63,27 @@ const ListToolBar = () => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
useEffect(() => {
|
||||||
const today = new Date();
|
try {
|
||||||
const threeMonthsAgo = getOneMonthsAgo();
|
table.getColumn('created_at')?.setFilterValue(dateRange);
|
||||||
const resetDateRange = {
|
} catch (error) {
|
||||||
from: formatDate(threeMonthsAgo),
|
toast.error('Error applying date filter');
|
||||||
to: formatDate(today)
|
console.error('Date filter error:', error);
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkNewDay = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const formattedNow = formatDate(now);
|
||||||
|
if (formattedNow !== dateRange.from || formattedNow !== dateRange.to) {
|
||||||
|
setDateRange({ from: formattedNow, to: formattedNow });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setSearchValue('');
|
const interval = setInterval(checkNewDay, 60 * 1000);
|
||||||
setDateRange(resetDateRange);
|
return () => clearInterval(interval);
|
||||||
|
}, [dateRange]);
|
||||||
table.setColumnFilters([{ id: 'created_at', value: resetDateRange }]);
|
|
||||||
table.setPageIndex(0);
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||||
|
|||||||
97
src/pages/notification/blocks/ShowDetailDialog.tsx
Normal file
97
src/pages/notification/blocks/ShowDetailDialog.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useManageNotificationContext } from '../hooks/useManageNotificationContext';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const ShowDialog = () => {
|
||||||
|
const { showDetailDialog, setShowDetailDialog, selectedNotification } =
|
||||||
|
useManageNotificationContext();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
|
||||||
|
<DialogContent className="max-w-[95vw] sm:max-w-[1000px] p-4 overflow-hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Notification Details</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogBody>
|
||||||
|
<div className="py-4 max-h-[70vh] overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center text-gray-500">Loading details...</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Mobile View (Card Layout) */}
|
||||||
|
<div className="md:hidden space-y-3">
|
||||||
|
{selectedNotification ? (
|
||||||
|
<div className="border rounded-lg p-4 space-y-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500">Send</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{selectedNotification.all_customers === 'Y'
|
||||||
|
? 'All Customers'
|
||||||
|
: selectedNotification.customer_name || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500">Content</p>
|
||||||
|
<p className="text-sm break-words whitespace-normal">
|
||||||
|
{selectedNotification.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Field lainnya... */}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop View (Table) */}
|
||||||
|
<div className="hidden md:block overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 text-left">
|
||||||
|
<th className="p-3 text-sm min-w-[120px] max-w-[180px]">Send</th>
|
||||||
|
<th className="p-3 text-sm min-w-[200px]">Content</th>
|
||||||
|
<th className="p-3 text-sm min-w-[100px]">Subject</th>
|
||||||
|
<th className="p-3 text-sm min-w-[80px]">Type</th>
|
||||||
|
<th className="p-3 text-sm min-w-[80px]">Via</th>
|
||||||
|
<th className="p-3 text-sm min-w-[150px]">Date Create</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{selectedNotification && (
|
||||||
|
<tr className="border-b">
|
||||||
|
<td className="p-3 text-sm max-w-[180px] overflow-hidden text-ellipsis">
|
||||||
|
{selectedNotification.all_customers === 'Y'
|
||||||
|
? 'All Customers'
|
||||||
|
: selectedNotification.customer_name || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-sm break-words whitespace-pre-wrap max-w-[300px]">
|
||||||
|
{selectedNotification.content}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-sm">{selectedNotification.subject}</td>
|
||||||
|
<td className="p-3 text-sm">{selectedNotification.type}</td>
|
||||||
|
<td className="p-3 text-sm">{selectedNotification.via}</td>
|
||||||
|
<td className="p-3 text-sm">
|
||||||
|
{moment(selectedNotification.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowDialog;
|
||||||
@ -6,18 +6,23 @@ import React, { createContext, useCallback, useMemo, useState } from 'react';
|
|||||||
import { ListToolBar } from '../blocks/ListToolbar';
|
import { ListToolBar } from '../blocks/ListToolbar';
|
||||||
import { useCallApi } from '@/hooks';
|
import { useCallApi } from '@/hooks';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import ShowDetailDialog from '../blocks/ShowDetailDialog';
|
||||||
|
|
||||||
interface ContextProps {
|
interface ContextProps {
|
||||||
|
showDetailDialog: boolean;
|
||||||
|
setShowDetailDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
showAddDialog: boolean;
|
showAddDialog: boolean;
|
||||||
handleAddDialog: (show: boolean) => void;
|
handleAddDialog: (show: boolean) => void;
|
||||||
showEditDialog: boolean;
|
showEditDialog: boolean;
|
||||||
handleEditDialog: (show: boolean, selected_notification: string | null) => void;
|
handleEditDialog: (show: boolean, selected_notification: string | null) => void;
|
||||||
showDeleteDialog: boolean;
|
showDeleteDialog: boolean;
|
||||||
handleDeleteDialog: (show: boolean, selected_notification: string | null) => void;
|
handleDeleteDialog: (show: boolean, selected_notification: string | null) => void;
|
||||||
selectedNotification: string | null;
|
selectedNotification: any | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialProps: ContextProps = {
|
const initialProps: ContextProps = {
|
||||||
|
showDetailDialog: false,
|
||||||
|
setShowDetailDialog: () => {},
|
||||||
showAddDialog: false,
|
showAddDialog: false,
|
||||||
handleAddDialog: (show: boolean) => {},
|
handleAddDialog: (show: boolean) => {},
|
||||||
showEditDialog: false,
|
showEditDialog: false,
|
||||||
@ -31,10 +36,11 @@ const ManageNotifContext = createContext<ContextProps>(initialProps);
|
|||||||
const API_URL_NOTIFICATION = apiConfig.service_notification;
|
const API_URL_NOTIFICATION = apiConfig.service_notification;
|
||||||
|
|
||||||
const ManageNotifContextProvider = ({ children }: { children: React.ReactNode }) => {
|
const ManageNotifContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [selectedNotification, setSelectedNotification] = useState<string | null>(null);
|
const [selectedNotification, setSelectedNotification] = useState<any | null>(null);
|
||||||
const { GetData } = useCallApi();
|
const { GetData } = useCallApi();
|
||||||
|
|
||||||
const handleAddDialog = useCallback((show: boolean) => {
|
const handleAddDialog = useCallback((show: boolean) => {
|
||||||
@ -77,6 +83,20 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
meta: {
|
meta: {
|
||||||
headerClassName: 'w-[350px]',
|
headerClassName: 'w-[350px]',
|
||||||
searchable: true
|
searchable: true
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const content = row.original.content;
|
||||||
|
const paragraphs = content.split(/\n+/);
|
||||||
|
|
||||||
|
const preview = paragraphs.slice(0, 2).join('\n');
|
||||||
|
const isTruncated = paragraphs.length > 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{preview}
|
||||||
|
{isTruncated && '...'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,6 +127,29 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row }) => moment(row.original.created_at).format('YYYY-MM-DD HH:mm:ss')
|
cell: ({ row }) => moment(row.original.created_at).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'detail',
|
||||||
|
id: 'detail',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="Actions" column={column} />,
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
cell: (data) => {
|
||||||
|
const row = data.row.original;
|
||||||
|
return (
|
||||||
|
<div key={`actions-${row.id}`} className="flex gap-2 justify-center">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedNotification(row);
|
||||||
|
setShowDetailDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeenIcon icon="eye" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[handleEditDialog, handleDeleteDialog]
|
[handleEditDialog, handleDeleteDialog]
|
||||||
@ -156,6 +199,8 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
<div>
|
<div>
|
||||||
<ManageNotifContext.Provider
|
<ManageNotifContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
showDetailDialog,
|
||||||
|
setShowDetailDialog,
|
||||||
handleAddDialog,
|
handleAddDialog,
|
||||||
showAddDialog,
|
showAddDialog,
|
||||||
handleEditDialog,
|
handleEditDialog,
|
||||||
@ -167,6 +212,8 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
>
|
>
|
||||||
<Toaster expand visibleToasts={9} duration={3000} />
|
<Toaster expand visibleToasts={9} duration={3000} />
|
||||||
|
|
||||||
|
<ShowDetailDialog />
|
||||||
|
|
||||||
<DataGridProvider
|
<DataGridProvider
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{ size: 10 }}
|
pagination={{ size: 10 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user