From 1b5510b4e56a8f1a8710e34b152c4cad2c8b6e0c Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 5 Aug 2026 13:46:38 +0700 Subject: [PATCH] feat: add reusable components for dialogs and headers - Implemented DeleteConfirmDialog for confirming deletions. - Created FormDialog for handling forms within dialogs. - Added PageHeader component for consistent page headers. - Introduced RowActions component for action buttons with tooltips. - Added useServerTable hook for managing server-side table interactions. - Updated DatePicker component with default month and dropdown caption layout. - Removed outdated data.json file. --- resources/js/components/data-table.tsx | 1190 +++++++---------- resources/js/components/date-picker.tsx | 2 + .../js/components/delete-confirm-dialog.tsx | 39 + resources/js/components/form-dialog.tsx | 81 ++ resources/js/components/page-header.tsx | 21 + resources/js/components/row-actions.tsx | 67 + resources/js/hooks/use-server-table.tsx | 118 ++ resources/js/pages/data/data.json | 614 --------- 8 files changed, 800 insertions(+), 1332 deletions(-) create mode 100644 resources/js/components/delete-confirm-dialog.tsx create mode 100644 resources/js/components/form-dialog.tsx create mode 100644 resources/js/components/page-header.tsx create mode 100644 resources/js/components/row-actions.tsx create mode 100644 resources/js/hooks/use-server-table.tsx delete mode 100644 resources/js/pages/data/data.json diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index c05afc1..e5bce2e 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -1,94 +1,43 @@ -import * as React from "react" +import type { DragEndEvent } from '@dnd-kit/core'; import { - closestCenter, DndContext, KeyboardSensor, - MouseSensor, - TouchSensor, + PointerSensor, useSensor, useSensors, - type DragEndEvent, - type UniqueIdentifier, -} from "@dnd-kit/core" -import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +} from '@dnd-kit/core'; import { - arrayMove, SortableContext, useSortable, verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { - IconChevronDown, - IconChevronLeft, - IconChevronRight, - IconChevronsLeft, - IconChevronsRight, - IconCircleCheckFilled, - IconDotsVertical, - IconGripVertical, - IconLayoutColumns, - IconLoader, - IconPlus, - IconTrendingUp, -} from "@tabler/icons-react" +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import type { ColumnDef, ExpandedState, Row } from '@tanstack/react-table'; import { flexRender, getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, + getExpandedRowModel, useReactTable, - type ColumnDef, - type ColumnFiltersState, - type Row, - type SortingState, - type VisibilityState, -} from "@tanstack/react-table" -import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" -import { toast } from "sonner" -import { z } from "zod" +} from '@tanstack/react-table'; +import { + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + GripVertical, +} from 'lucide-react'; +import * as React from 'react'; -import { useIsMobile } from "@/hooks/use-mobile" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - type ChartConfig, -} from "@/components/ui/chart" -import { Checkbox } from "@/components/ui/checkbox" -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" +} from '@/components/ui/select'; import { Table, TableBody, @@ -96,710 +45,515 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" +} from '@/components/ui/table'; -export const schema = z.object({ - id: z.number(), - header: z.string(), - type: z.string(), - status: z.string(), - target: z.string(), - limit: z.string(), - reviewer: z.string(), -}) +export interface PaginationState { + current_page: number; + last_page: number; + per_page: number; + total: number; +} -// Create a separate component for the drag handle -function DragHandle({ id }: { id: number }) { - const { attributes, listeners } = useSortable({ - id, - }) +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + searchKey?: string; + searchPlaceholder?: string; + emptyText?: string; + onReorder?: (items: TData[]) => void; + getRowId?: (item: TData) => string | number; + toolbar?: React.ReactNode; + renderSubRow?: (row: Row, searchValue?: string) => React.ReactNode; + defaultExpanded?: boolean; + pagination?: PaginationState; + onPageChange?: (page: number) => void; + onPerPageChange?: (perPage: number) => void; + onSearchChange?: (search: string) => void; + searchValue?: string; +} + +const DragHandleContext = React.createContext<{ + listeners?: Record; + attributes?: Record; +}>({}); + +export function DragHandleTrigger() { + const { listeners, attributes } = React.useContext(DragHandleContext); return ( - ) + ); } -const columns: ColumnDef>[] = [ - { - id: "drag", - header: () => null, - cell: ({ row }) => , - }, - { - id: "select", - header: ({ table }) => ( -
- table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> -
- ), - cell: ({ row }) => ( -
- row.toggleSelected(!!value)} - aria-label="Select row" - /> -
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "header", - header: "Header", - cell: ({ row }) => { - return - }, - enableHiding: false, - }, - { - accessorKey: "type", - header: "Section Type", - cell: ({ row }) => ( -
- - {row.original.type} - -
- ), - }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => ( - - {row.original.status === "Done" ? ( - - ) : ( - - )} - {row.original.status} - - ), - }, - { - accessorKey: "target", - header: () =>
Target
, - cell: ({ row }) => ( -
{ - e.preventDefault() - toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { - loading: `Saving ${row.original.header}`, - success: "Done", - error: "Error", - }) - }} - > - - -
- ), - }, - { - accessorKey: "limit", - header: () =>
Limit
, - cell: ({ row }) => ( -
{ - e.preventDefault() - toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { - loading: `Saving ${row.original.header}`, - success: "Done", - error: "Error", - }) - }} - > - - -
- ), - }, - { - accessorKey: "reviewer", - header: "Reviewer", - cell: ({ row }) => { - const isAssigned = row.original.reviewer !== "Assign reviewer" +function SortableTableRow({ + id, + children, +}: { + id: string; + children: React.ReactNode; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); - if (isAssigned) { - return row.original.reviewer - } - - return ( - <> - - - - ) - }, - }, - { - id: "actions", - cell: () => ( - - - - - - Edit - Make a copy - Favorite - - Delete - - - ), - }, -] - -function DraggableRow({ row }: { row: Row> }) { - const { transform, transition, setNodeRef, isDragging } = useSortable({ - id: row.original.id, - }) + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.4 : undefined, + }; return ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ) + + + {children} + + + ); } -export function DataTable({ - data: initialData, -}: { - data: z.infer[] -}) { - const [data, setData] = React.useState(() => initialData) - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, - }) - const sortableId = React.useId() - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) +function useDebounce(callback: (value: string) => void, delay: number) { + const timeoutRef = React.useRef | null>(null); - const dataIds = React.useMemo( - () => data?.map(({ id }) => id) || [], - [data] - ) + return React.useCallback( + (value: string) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + callback(value); + }, delay); + }, + [callback, delay], + ); +} + +export function DataTable({ + columns, + data, + searchKey, + searchPlaceholder = 'Cari...', + emptyText = 'Tidak ada data.', + onReorder, + getRowId, + toolbar, + renderSubRow, + defaultExpanded = false, + pagination, + onPageChange, + onPerPageChange, + onSearchChange, + searchValue, +}: DataTableProps) { + const [expanded, setExpanded] = React.useState(() => { + if (!defaultExpanded || !data.length) { + return {}; + } + + const initial: Record = {}; + data.forEach((item, index) => { + initial[String(index)] = true; + }); + + return initial; + }); + + const [localSearch, setLocalSearch] = React.useState(searchValue ?? ''); + + React.useEffect(() => { + setLocalSearch(searchValue ?? ''); + }, [searchValue]); + + const isServerMode = !!pagination && !!onPageChange; + const isSortable = !!onReorder && !!getRowId; + + const handleSearchDebounced = useDebounce( + (value: string) => onSearchChange?.(value), + 300, + ); + + const totalPages = pagination?.last_page ?? 1; + const currentPage = pagination?.current_page ?? 1; + + const visibleColumns = [ + { + id: 'no', + header: () => No, + cell: ({ row }) => ( + + {isServerMode + ? (currentPage - 1) * (pagination?.per_page ?? 25) + + row.index + + 1 + : row.index + 1} + + ), + meta: { + className: 'w-[50px] text-center', + headerClassName: 'w-[50px] text-center', + }, + } as ColumnDef, + ...(isSortable + ? [ + { + id: 'drag', + header: '', + cell: () => , + meta: { + className: 'w-[40px]', + headerClassName: 'w-[40px]', + }, + } as ColumnDef, + ] + : []), + ...columns, + ]; + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 8 }, + }), + useSensor(KeyboardSensor), + ); const table = useReactTable({ data, - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - pagination, - }, - getRowId: (row) => row.id.toString(), - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, + columns: visibleColumns, getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }) + getExpandedRowModel: renderSubRow ? getExpandedRowModel() : undefined, + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); + + const itemIds = React.useMemo( + () => data.map((item) => String(getRowId ? getRowId(item) : '')), + [data, getRowId], + ); function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - if (active && over && active.id !== over.id) { - setData((data) => { - const oldIndex = dataIds.indexOf(active.id) - const newIndex = dataIds.indexOf(over.id) - return arrayMove(data, oldIndex, newIndex) - }) + const { active, over } = event; + + if (!over || active.id === over.id) { + return; + } + + const oldIndex = itemIds.indexOf(String(active.id)); + const newIndex = itemIds.indexOf(String(over.id)); + + if (oldIndex === -1 || newIndex === -1) { + return; + } + + const reordered = [...data]; + const [moved] = reordered.splice(oldIndex, 1); + reordered.splice(newIndex, 0, moved); + + onReorder?.(reordered); + } + + function handleSearchChange(value: string) { + setLocalSearch(value); + + if (isServerMode) { + handleSearchDebounced(value); } } return ( - -
- - - - Outline - - Past Performance 3 - - - Key Personnel 2 - - Focus Documents - + <> + {(searchKey || toolbar || isServerMode) && (
- - - - - - {table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && - column.getCanHide() - ) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - - -
-
- -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - - No results. - - - )} - -
-
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
-
- + {searchKey && ( + + handleSearchChange(event.target.value) + } + className="max-w-sm" + /> + )} + {toolbar} +
+ {isServerMode && onPerPageChange && ( -
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
+ )} +
+
+ )} + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + isSortable ? ( + + + {table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext(), + )} + + ))} + + ))} + + + ) : ( + table.getRowModel().rows.map((row) => ( + + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell.column + .columnDef + .cell, + cell.getContext(), + )} + + ))} + + {renderSubRow && + row.getIsExpanded() && ( + + +
+ {renderSubRow( + row, + localSearch, + )} +
+
+
+ )} +
+ )) + ) + ) : ( + + + {emptyText} + + + )} +
+
+
+
+ +
+ + {isServerMode + ? `Halaman ${currentPage} dari ${totalPages}` + : `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`} + +
+ {isServerMode ? ( + <> + + + + + ) : ( + <> + -
-
-
- - -
-
- -
-
- -
-
- - ) -} - -const chartData = [ - { month: "January", desktop: 186, mobile: 80 }, - { month: "February", desktop: 305, mobile: 200 }, - { month: "March", desktop: 237, mobile: 120 }, - { month: "April", desktop: 73, mobile: 190 }, - { month: "May", desktop: 209, mobile: 130 }, - { month: "June", desktop: 214, mobile: 140 }, -] - -const chartConfig = { - desktop: { - label: "Desktop", - color: "var(--primary)", - }, - mobile: { - label: "Mobile", - color: "var(--primary)", - }, -} satisfies ChartConfig - -function TableCellViewer({ item }: { item: z.infer }) { - const isMobile = useIsMobile() - - return ( - - - - - - - {item.header} - - Showing total visitors for the last 6 months - - -
- {!isMobile && ( - <> - - - - value.slice(0, 3)} - hide - /> - } - /> - - - - - -
-
- Trending up by 5.2% this month{" "} - -
-
- Showing total visitors for the last 6 months. This is just - some random text to test the layout. It spans multiple lines - and should wrap around. -
-
- )} -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
- - - - - - -
-
- ) +
+ + ); } diff --git a/resources/js/components/date-picker.tsx b/resources/js/components/date-picker.tsx index 6d1dba2..ab651e4 100644 --- a/resources/js/components/date-picker.tsx +++ b/resources/js/components/date-picker.tsx @@ -45,6 +45,8 @@ export function DatePicker({ diff --git a/resources/js/components/delete-confirm-dialog.tsx b/resources/js/components/delete-confirm-dialog.tsx new file mode 100644 index 0000000..e03b23e --- /dev/null +++ b/resources/js/components/delete-confirm-dialog.tsx @@ -0,0 +1,39 @@ +import { ConfirmDialog } from '@/components/confirm-dialog'; + +type DeleteConfirmDialogProps = { + target: T | null; + onOpenChange: (open: boolean) => void; + title: string; + description?: string | ((target: T) => string); + confirmLabel?: string; + variant?: 'default' | 'destructive'; + onConfirm: () => void; +}; + +export function DeleteConfirmDialog({ + target, + onOpenChange, + title, + description, + confirmLabel = 'Hapus', + variant, + onConfirm, +}: DeleteConfirmDialogProps) { + return ( + + ); +} diff --git a/resources/js/components/form-dialog.tsx b/resources/js/components/form-dialog.tsx new file mode 100644 index 0000000..e682027 --- /dev/null +++ b/resources/js/components/form-dialog.tsx @@ -0,0 +1,81 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Form } from '@inertiajs/react'; +import type { ReactNode } from 'react'; + +type FormDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + action: React.ComponentProps['action']; + resetOnSuccess?: boolean; + onSuccess?: () => void; + submitDisabled?: boolean; + submitLabel?: ReactNode; + submittingLabel?: ReactNode; + children: + | ReactNode + | ((ctx: { + errors: Record; + processing: boolean; + }) => ReactNode); +}; + +export function FormDialog({ + open, + onOpenChange, + title, + action, + resetOnSuccess, + onSuccess, + submitDisabled = false, + submitLabel = 'Simpan', + submittingLabel = 'Menyimpan...', + children, +}: FormDialogProps) { + return ( + + +
+ {({ errors, processing }) => ( + <> + + {title} + +
+ {typeof children === 'function' + ? children({ errors, processing }) + : children} +
+ + + + + + )} +
+
+
+ ); +} diff --git a/resources/js/components/page-header.tsx b/resources/js/components/page-header.tsx new file mode 100644 index 0000000..3e8c425 --- /dev/null +++ b/resources/js/components/page-header.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; + +type PageHeaderProps = { + title: string; + description?: ReactNode; + actions?: ReactNode; +}; + +export function PageHeader({ title, description, actions }: PageHeaderProps) { + return ( +
+
+

+ {title} +

+ {description} +
+ {actions} +
+ ); +} diff --git a/resources/js/components/row-actions.tsx b/resources/js/components/row-actions.tsx new file mode 100644 index 0000000..33e5b76 --- /dev/null +++ b/resources/js/components/row-actions.tsx @@ -0,0 +1,67 @@ +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { Link } from '@inertiajs/react'; +import type { ReactNode } from 'react'; + +export type RowAction = { + label: string; + icon: ReactNode; + iconClassName?: string; + show?: boolean; + onClick?: () => void; + href?: string; +}; + +type RowActionsProps = { + actions: RowAction[]; + wrapperClassName?: string; +}; + +export function RowActions({ + actions, + wrapperClassName = 'flex items-center justify-center gap-1', +}: RowActionsProps) { + return ( + +
+ {actions + .filter((action) => action.show !== false) + .map((action) => ( + + + {action.href ? ( + + ) : ( + + )} + + + {action.label} + + + ))} +
+
+ ); +} diff --git a/resources/js/hooks/use-server-table.tsx b/resources/js/hooks/use-server-table.tsx new file mode 100644 index 0000000..8b50624 --- /dev/null +++ b/resources/js/hooks/use-server-table.tsx @@ -0,0 +1,118 @@ +import type { PaginationState } from '@/components/data-table'; +import { router } from '@inertiajs/react'; +import { useCallback, useState } from 'react'; + +type UseServerTableOptions = { + route: () => string; + pagination: PaginationState; + filters?: Record; + filterWithParams?: boolean; +}; + +export function useServerTable({ + route, + pagination, + filters, + filterWithParams = true, +}: UseServerTableOptions) { + const [search, setSearch] = useState(''); + const [filterOpen, setFilterOpen] = useState(false); + + const handlePageChange = useCallback( + (page: number) => { + router.get( + route(), + { + ...filters, + page, + per_page: pagination.per_page, + search, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, pagination.per_page, search], + ); + + const handlePerPageChange = useCallback( + (perPage: number) => { + router.get( + route(), + { + ...filters, + page: 1, + per_page: perPage, + search, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, search], + ); + + const handleSearchChange = useCallback( + (value: string) => { + setSearch(value); + router.get( + route(), + { + ...filters, + page: 1, + per_page: pagination.per_page, + search: value, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, pagination.per_page], + ); + + function applyFilter(key: string, value: string) { + const newFilters = { ...filters }; + + if (value === '' || value === 'all') { + delete newFilters[key]; + } else { + newFilters[key] = value; + } + + router.get( + route(), + filterWithParams + ? { + ...newFilters, + page: 1, + per_page: pagination.per_page, + search, + } + : newFilters, + { preserveState: true, replace: true }, + ); + } + + function clearFilters() { + router.get( + route(), + filterWithParams + ? { + page: 1, + per_page: pagination.per_page, + search, + } + : {}, + { preserveState: true, replace: true }, + ); + setFilterOpen(false); + } + + return { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + }; +} diff --git a/resources/js/pages/data/data.json b/resources/js/pages/data/data.json deleted file mode 100644 index ec08736..0000000 --- a/resources/js/pages/data/data.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "id": 1, - "header": "Cover page", - "type": "Cover page", - "status": "In Process", - "target": "18", - "limit": "5", - "reviewer": "Eddie Lake" - }, - { - "id": 2, - "header": "Table of contents", - "type": "Table of contents", - "status": "Done", - "target": "29", - "limit": "24", - "reviewer": "Eddie Lake" - }, - { - "id": 3, - "header": "Executive summary", - "type": "Narrative", - "status": "Done", - "target": "10", - "limit": "13", - "reviewer": "Eddie Lake" - }, - { - "id": 4, - "header": "Technical approach", - "type": "Narrative", - "status": "Done", - "target": "27", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 5, - "header": "Design", - "type": "Narrative", - "status": "In Process", - "target": "2", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 6, - "header": "Capabilities", - "type": "Narrative", - "status": "In Process", - "target": "20", - "limit": "8", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 7, - "header": "Integration with existing systems", - "type": "Narrative", - "status": "In Process", - "target": "19", - "limit": "21", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 8, - "header": "Innovation and Advantages", - "type": "Narrative", - "status": "Done", - "target": "25", - "limit": "26", - "reviewer": "Assign reviewer" - }, - { - "id": 9, - "header": "Overview of EMR's Innovative Solutions", - "type": "Technical content", - "status": "Done", - "target": "7", - "limit": "23", - "reviewer": "Assign reviewer" - }, - { - "id": 10, - "header": "Advanced Algorithms and Machine Learning", - "type": "Narrative", - "status": "Done", - "target": "30", - "limit": "28", - "reviewer": "Assign reviewer" - }, - { - "id": 11, - "header": "Adaptive Communication Protocols", - "type": "Narrative", - "status": "Done", - "target": "9", - "limit": "31", - "reviewer": "Assign reviewer" - }, - { - "id": 12, - "header": "Advantages Over Current Technologies", - "type": "Narrative", - "status": "Done", - "target": "12", - "limit": "0", - "reviewer": "Assign reviewer" - }, - { - "id": 13, - "header": "Past Performance", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "33", - "reviewer": "Assign reviewer" - }, - { - "id": 14, - "header": "Customer Feedback and Satisfaction Levels", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "34", - "reviewer": "Assign reviewer" - }, - { - "id": 15, - "header": "Implementation Challenges and Solutions", - "type": "Narrative", - "status": "Done", - "target": "3", - "limit": "35", - "reviewer": "Assign reviewer" - }, - { - "id": 16, - "header": "Security Measures and Data Protection Policies", - "type": "Narrative", - "status": "In Process", - "target": "6", - "limit": "36", - "reviewer": "Assign reviewer" - }, - { - "id": 17, - "header": "Scalability and Future Proofing", - "type": "Narrative", - "status": "Done", - "target": "4", - "limit": "37", - "reviewer": "Assign reviewer" - }, - { - "id": 18, - "header": "Cost-Benefit Analysis", - "type": "Plain language", - "status": "Done", - "target": "14", - "limit": "38", - "reviewer": "Assign reviewer" - }, - { - "id": 19, - "header": "User Training and Onboarding Experience", - "type": "Narrative", - "status": "Done", - "target": "17", - "limit": "39", - "reviewer": "Assign reviewer" - }, - { - "id": 20, - "header": "Future Development Roadmap", - "type": "Narrative", - "status": "Done", - "target": "11", - "limit": "40", - "reviewer": "Assign reviewer" - }, - { - "id": 21, - "header": "System Architecture Overview", - "type": "Technical content", - "status": "In Process", - "target": "24", - "limit": "18", - "reviewer": "Maya Johnson" - }, - { - "id": 22, - "header": "Risk Management Plan", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "22", - "reviewer": "Carlos Rodriguez" - }, - { - "id": 23, - "header": "Compliance Documentation", - "type": "Legal", - "status": "In Process", - "target": "31", - "limit": "27", - "reviewer": "Sarah Chen" - }, - { - "id": 24, - "header": "API Documentation", - "type": "Technical content", - "status": "Done", - "target": "8", - "limit": "12", - "reviewer": "Raj Patel" - }, - { - "id": 25, - "header": "User Interface Mockups", - "type": "Visual", - "status": "In Process", - "target": "19", - "limit": "25", - "reviewer": "Leila Ahmadi" - }, - { - "id": 26, - "header": "Database Schema", - "type": "Technical content", - "status": "Done", - "target": "22", - "limit": "20", - "reviewer": "Thomas Wilson" - }, - { - "id": 27, - "header": "Testing Methodology", - "type": "Technical content", - "status": "In Process", - "target": "17", - "limit": "14", - "reviewer": "Assign reviewer" - }, - { - "id": 28, - "header": "Deployment Strategy", - "type": "Narrative", - "status": "Done", - "target": "26", - "limit": "30", - "reviewer": "Eddie Lake" - }, - { - "id": 29, - "header": "Budget Breakdown", - "type": "Financial", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 30, - "header": "Market Analysis", - "type": "Research", - "status": "Done", - "target": "29", - "limit": "32", - "reviewer": "Sophia Martinez" - }, - { - "id": 31, - "header": "Competitor Comparison", - "type": "Research", - "status": "In Process", - "target": "21", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 32, - "header": "Maintenance Plan", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "23", - "reviewer": "Alex Thompson" - }, - { - "id": 33, - "header": "User Personas", - "type": "Research", - "status": "In Process", - "target": "27", - "limit": "24", - "reviewer": "Nina Patel" - }, - { - "id": 34, - "header": "Accessibility Compliance", - "type": "Legal", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 35, - "header": "Performance Metrics", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "David Kim" - }, - { - "id": 36, - "header": "Disaster Recovery Plan", - "type": "Technical content", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 37, - "header": "Third-party Integrations", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Eddie Lake" - }, - { - "id": 38, - "header": "User Feedback Summary", - "type": "Research", - "status": "Done", - "target": "20", - "limit": "15", - "reviewer": "Assign reviewer" - }, - { - "id": 39, - "header": "Localization Strategy", - "type": "Narrative", - "status": "In Process", - "target": "12", - "limit": "19", - "reviewer": "Maria Garcia" - }, - { - "id": 40, - "header": "Mobile Compatibility", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "James Wilson" - }, - { - "id": 41, - "header": "Data Migration Plan", - "type": "Technical content", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Assign reviewer" - }, - { - "id": 42, - "header": "Quality Assurance Protocols", - "type": "Technical content", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Priya Singh" - }, - { - "id": 43, - "header": "Stakeholder Analysis", - "type": "Research", - "status": "In Process", - "target": "11", - "limit": "14", - "reviewer": "Eddie Lake" - }, - { - "id": 44, - "header": "Environmental Impact Assessment", - "type": "Research", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Assign reviewer" - }, - { - "id": 45, - "header": "Intellectual Property Rights", - "type": "Legal", - "status": "In Process", - "target": "17", - "limit": "20", - "reviewer": "Sarah Johnson" - }, - { - "id": 46, - "header": "Customer Support Framework", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 47, - "header": "Version Control Strategy", - "type": "Technical content", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 48, - "header": "Continuous Integration Pipeline", - "type": "Technical content", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Michael Chen" - }, - { - "id": 49, - "header": "Regulatory Compliance", - "type": "Legal", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Assign reviewer" - }, - { - "id": 50, - "header": "User Authentication System", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "Eddie Lake" - }, - { - "id": 51, - "header": "Data Analytics Framework", - "type": "Technical content", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 52, - "header": "Cloud Infrastructure", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 53, - "header": "Network Security Measures", - "type": "Technical content", - "status": "In Process", - "target": "29", - "limit": "32", - "reviewer": "Lisa Wong" - }, - { - "id": 54, - "header": "Project Timeline", - "type": "Planning", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Eddie Lake" - }, - { - "id": 55, - "header": "Resource Allocation", - "type": "Planning", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Assign reviewer" - }, - { - "id": 56, - "header": "Team Structure and Roles", - "type": "Planning", - "status": "Done", - "target": "20", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 57, - "header": "Communication Protocols", - "type": "Planning", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 58, - "header": "Success Metrics", - "type": "Planning", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Eddie Lake" - }, - { - "id": 59, - "header": "Internationalization Support", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 60, - "header": "Backup and Recovery Procedures", - "type": "Technical content", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 61, - "header": "Monitoring and Alerting System", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Daniel Park" - }, - { - "id": 62, - "header": "Code Review Guidelines", - "type": "Technical content", - "status": "Done", - "target": "12", - "limit": "15", - "reviewer": "Eddie Lake" - }, - { - "id": 63, - "header": "Documentation Standards", - "type": "Technical content", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 64, - "header": "Release Management Process", - "type": "Planning", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Assign reviewer" - }, - { - "id": 65, - "header": "Feature Prioritization Matrix", - "type": "Planning", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Emma Davis" - }, - { - "id": 66, - "header": "Technical Debt Assessment", - "type": "Technical content", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Eddie Lake" - }, - { - "id": 67, - "header": "Capacity Planning", - "type": "Planning", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 68, - "header": "Service Level Agreements", - "type": "Legal", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Assign reviewer" - } -] -- 2.45.2