init
This commit is contained in:
186
frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx
Normal file
186
frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { useIdeasStore } from '../../store/ideas';
|
||||
import { useCreateIdea } from '../../hooks/useIdeas';
|
||||
import type { CreateIdeaDto, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
|
||||
const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||
{ value: 'backlog', label: 'Backlog' },
|
||||
{ value: 'todo', label: 'To Do' },
|
||||
{ value: 'in_progress', label: 'In Progress' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
];
|
||||
|
||||
const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'critical', label: 'Critical' },
|
||||
];
|
||||
|
||||
const initialFormData: CreateIdeaDto = {
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'backlog',
|
||||
priority: 'medium',
|
||||
module: '',
|
||||
targetAudience: '',
|
||||
pain: '',
|
||||
aiRole: '',
|
||||
verificationMethod: '',
|
||||
};
|
||||
|
||||
export function CreateIdeaModal() {
|
||||
const { createModalOpen, setCreateModalOpen } = useIdeasStore();
|
||||
const createIdea = useCreateIdea();
|
||||
const [formData, setFormData] = useState<CreateIdeaDto>(initialFormData);
|
||||
|
||||
const handleClose = () => {
|
||||
setCreateModalOpen(false);
|
||||
setFormData(initialFormData);
|
||||
createIdea.reset();
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof CreateIdeaDto, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await createIdea.mutateAsync(formData);
|
||||
handleClose();
|
||||
} catch {
|
||||
// Error is handled by mutation state
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={createModalOpen} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogTitle>Create New Idea</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
||||
{createIdea.isError && (
|
||||
<Alert severity="error">
|
||||
Failed to create idea. Please try again.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Title"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={formData.status}
|
||||
label="Status"
|
||||
onChange={(e) => handleChange('status', e.target.value)}
|
||||
>
|
||||
{statusOptions.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Priority</InputLabel>
|
||||
<Select
|
||||
value={formData.priority}
|
||||
label="Priority"
|
||||
onChange={(e) => handleChange('priority', e.target.value)}
|
||||
>
|
||||
{priorityOptions.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Module"
|
||||
value={formData.module}
|
||||
onChange={(e) => handleChange('module', e.target.value)}
|
||||
placeholder="e.g., Auth, Dashboard, API"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Target Audience"
|
||||
value={formData.targetAudience}
|
||||
onChange={(e) => handleChange('targetAudience', e.target.value)}
|
||||
placeholder="Who is this for?"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Pain Point"
|
||||
value={formData.pain}
|
||||
onChange={(e) => handleChange('pain', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
placeholder="What problem does this solve?"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="AI Role"
|
||||
value={formData.aiRole}
|
||||
onChange={(e) => handleChange('aiRole', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
placeholder="How can AI help with this?"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Verification Method"
|
||||
value={formData.verificationMethod}
|
||||
onChange={(e) => handleChange('verificationMethod', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
placeholder="How to verify this is done?"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={!formData.title || createIdea.isPending}
|
||||
>
|
||||
{createIdea.isPending ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
1
frontend/src/components/CreateIdeaModal/index.ts
Normal file
1
frontend/src/components/CreateIdeaModal/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { CreateIdeaModal } from './CreateIdeaModal';
|
||||
126
frontend/src/components/IdeasFilters/IdeasFilters.tsx
Normal file
126
frontend/src/components/IdeasFilters/IdeasFilters.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Button,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { Search, Clear } from '@mui/icons-material';
|
||||
import { useIdeasStore } from '../../store/ideas';
|
||||
import { useModulesQuery } from '../../hooks/useIdeas';
|
||||
import type { IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
|
||||
const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||
{ value: 'backlog', label: 'Backlog' },
|
||||
{ value: 'todo', label: 'To Do' },
|
||||
{ value: 'in_progress', label: 'In Progress' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
];
|
||||
|
||||
const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'critical', label: 'Critical' },
|
||||
];
|
||||
|
||||
export function IdeasFilters() {
|
||||
const { filters, setFilter, clearFilters } = useIdeasStore();
|
||||
const { data: modules = [] } = useModulesQuery();
|
||||
const [searchValue, setSearchValue] = useState(filters.search || '');
|
||||
|
||||
// Debounced search
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setFilter('search', searchValue || undefined);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchValue, setFilter]);
|
||||
|
||||
const hasFilters = filters.status || filters.priority || filters.module || filters.search;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search ideas..."
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ minWidth: 200 }}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Search fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={filters.status || ''}
|
||||
label="Status"
|
||||
onChange={(e) => setFilter('status', e.target.value as IdeaStatus || undefined)}
|
||||
>
|
||||
<MenuItem value="">All</MenuItem>
|
||||
{statusOptions.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>Priority</InputLabel>
|
||||
<Select
|
||||
value={filters.priority || ''}
|
||||
label="Priority"
|
||||
onChange={(e) => setFilter('priority', e.target.value as IdeaPriority || undefined)}
|
||||
>
|
||||
<MenuItem value="">All</MenuItem>
|
||||
{priorityOptions.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>Module</InputLabel>
|
||||
<Select
|
||||
value={filters.module || ''}
|
||||
label="Module"
|
||||
onChange={(e) => setFilter('module', e.target.value || undefined)}
|
||||
>
|
||||
<MenuItem value="">All</MenuItem>
|
||||
{modules.map((module) => (
|
||||
<MenuItem key={module} value={module}>
|
||||
{module}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{hasFilters && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Clear />}
|
||||
onClick={() => {
|
||||
clearFilters();
|
||||
setSearchValue('');
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
frontend/src/components/IdeasFilters/index.ts
Normal file
1
frontend/src/components/IdeasFilters/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { IdeasFilters } from './IdeasFilters';
|
||||
143
frontend/src/components/IdeasTable/EditableCell.tsx
Normal file
143
frontend/src/components/IdeasTable/EditableCell.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
ClickAwayListener,
|
||||
} from '@mui/material';
|
||||
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
import { useUpdateIdea } from '../../hooks/useIdeas';
|
||||
|
||||
interface EditableCellProps {
|
||||
idea: Idea;
|
||||
field: keyof Idea;
|
||||
value: string | null;
|
||||
type?: 'text' | 'select';
|
||||
options?: { value: string; label: string }[];
|
||||
renderDisplay: (value: string | null) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function EditableCell({
|
||||
idea,
|
||||
field,
|
||||
value,
|
||||
type = 'text',
|
||||
options,
|
||||
renderDisplay,
|
||||
}: EditableCellProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(value || '');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const updateIdea = useUpdateIdea();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
setIsEditing(true);
|
||||
setEditValue(value || '');
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsEditing(false);
|
||||
if (editValue !== value) {
|
||||
await updateIdea.mutateAsync({
|
||||
id: idea.id,
|
||||
dto: { [field]: editValue || null },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
setEditValue(value || '');
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
if (type === 'select' && options) {
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleSave}>
|
||||
<Select
|
||||
size="small"
|
||||
value={editValue}
|
||||
onChange={(e) => {
|
||||
setEditValue(e.target.value);
|
||||
setTimeout(() => {
|
||||
updateIdea.mutate({
|
||||
id: idea.id,
|
||||
dto: { [field]: e.target.value },
|
||||
});
|
||||
setIsEditing(false);
|
||||
}, 0);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
sx={{ minWidth: 100 }}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleSave}>
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
size="small"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
sx={{ minWidth: 100 }}
|
||||
/>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
onDoubleClick={handleDoubleClick}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
minHeight: 24,
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover',
|
||||
borderRadius: 0.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{renderDisplay(value)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Status options
|
||||
export const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||
{ value: 'backlog', label: 'Backlog' },
|
||||
{ value: 'todo', label: 'To Do' },
|
||||
{ value: 'in_progress', label: 'In Progress' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
];
|
||||
|
||||
// Priority options
|
||||
export const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'critical', label: 'Critical' },
|
||||
];
|
||||
182
frontend/src/components/IdeasTable/IdeasTable.tsx
Normal file
182
frontend/src/components/IdeasTable/IdeasTable.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
flexRender,
|
||||
} from '@tanstack/react-table';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
TableSortLabel,
|
||||
Skeleton,
|
||||
Box,
|
||||
Typography,
|
||||
TablePagination,
|
||||
} from '@mui/material';
|
||||
import { Inbox } from '@mui/icons-material';
|
||||
import { useIdeasQuery, useDeleteIdea } from '../../hooks/useIdeas';
|
||||
import { useIdeasStore } from '../../store/ideas';
|
||||
import { createColumns } from './columns';
|
||||
|
||||
const SKELETON_COLUMNS_COUNT = 7;
|
||||
|
||||
export function IdeasTable() {
|
||||
const { data, isLoading, isError } = useIdeasQuery();
|
||||
const deleteIdea = useDeleteIdea();
|
||||
const { sorting, setSorting, pagination, setPage, setLimit } = useIdeasStore();
|
||||
|
||||
const columns = useMemo(
|
||||
() => createColumns((id) => deleteIdea.mutate(id)),
|
||||
[deleteIdea]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: data?.data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualSorting: true,
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleSort = (columnId: string) => {
|
||||
setSorting(columnId);
|
||||
};
|
||||
|
||||
const handleChangePage = (_: unknown, newPage: number) => {
|
||||
setPage(newPage + 1);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLimit(parseInt(event.target.value, 10));
|
||||
};
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||||
<Typography color="error">Failed to load ideas</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
|
||||
<TableContainer>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableCell
|
||||
key={header.id}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
backgroundColor: 'grey.100',
|
||||
width: header.getSize(),
|
||||
}}
|
||||
>
|
||||
{header.column.getCanSort() ? (
|
||||
<TableSortLabel
|
||||
active={sorting.sortBy === header.id}
|
||||
direction={
|
||||
sorting.sortBy === header.id
|
||||
? (sorting.sortOrder.toLowerCase() as 'asc' | 'desc')
|
||||
: 'asc'
|
||||
}
|
||||
onClick={() => handleSort(header.id)}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
{Array.from({ length: SKELETON_COLUMNS_COUNT }).map((_, colIndex) => (
|
||||
<TableCell key={colIndex}>
|
||||
<Skeleton variant="text" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={SKELETON_COLUMNS_COUNT}>
|
||||
<Box
|
||||
sx={{
|
||||
py: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
<Inbox sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
|
||||
<Typography variant="h6">No ideas yet</Typography>
|
||||
<Typography variant="body2">
|
||||
Create your first idea to get started
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
hover
|
||||
sx={{
|
||||
backgroundColor: row.original.color
|
||||
? `${row.original.color}15`
|
||||
: undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: row.original.color
|
||||
? `${row.original.color}25`
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{data && (
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.meta.total}
|
||||
page={pagination.page - 1}
|
||||
rowsPerPage={pagination.limit}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
140
frontend/src/components/IdeasTable/columns.tsx
Normal file
140
frontend/src/components/IdeasTable/columns.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { Chip, Box, IconButton } from '@mui/material';
|
||||
import { Delete } from '@mui/icons-material';
|
||||
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||
import { EditableCell, statusOptions, priorityOptions } from './EditableCell';
|
||||
|
||||
const columnHelper = createColumnHelper<Idea>();
|
||||
|
||||
const statusColors: Record<IdeaStatus, 'default' | 'primary' | 'secondary' | 'success' | 'error'> = {
|
||||
backlog: 'default',
|
||||
todo: 'primary',
|
||||
in_progress: 'secondary',
|
||||
done: 'success',
|
||||
cancelled: 'error',
|
||||
};
|
||||
|
||||
const priorityColors: Record<IdeaPriority, 'default' | 'info' | 'warning' | 'error'> = {
|
||||
low: 'default',
|
||||
medium: 'info',
|
||||
high: 'warning',
|
||||
critical: 'error',
|
||||
};
|
||||
|
||||
export const createColumns = (onDelete: (id: string) => void) => [
|
||||
columnHelper.accessor('title', {
|
||||
header: 'Title',
|
||||
cell: (info) => (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="title"
|
||||
value={info.getValue()}
|
||||
renderDisplay={(value) => (
|
||||
<Box sx={{ fontWeight: 500 }}>{value || '—'}</Box>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
size: 250,
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: (info) => {
|
||||
const status = info.getValue();
|
||||
const label = statusOptions.find((s) => s.value === status)?.label || status;
|
||||
return (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="status"
|
||||
value={status}
|
||||
type="select"
|
||||
options={statusOptions}
|
||||
renderDisplay={() => (
|
||||
<Chip label={label} color={statusColors[status]} size="small" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
size: 140,
|
||||
}),
|
||||
columnHelper.accessor('priority', {
|
||||
header: 'Priority',
|
||||
cell: (info) => {
|
||||
const priority = info.getValue();
|
||||
const label = priorityOptions.find((p) => p.value === priority)?.label || priority;
|
||||
return (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="priority"
|
||||
value={priority}
|
||||
type="select"
|
||||
options={priorityOptions}
|
||||
renderDisplay={() => (
|
||||
<Chip
|
||||
label={label}
|
||||
color={priorityColors[priority]}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
}),
|
||||
columnHelper.accessor('module', {
|
||||
header: 'Module',
|
||||
cell: (info) => (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="module"
|
||||
value={info.getValue()}
|
||||
renderDisplay={(value) => value || '—'}
|
||||
/>
|
||||
),
|
||||
size: 120,
|
||||
}),
|
||||
columnHelper.accessor('targetAudience', {
|
||||
header: 'Target Audience',
|
||||
cell: (info) => (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="targetAudience"
|
||||
value={info.getValue()}
|
||||
renderDisplay={(value) => value || '—'}
|
||||
/>
|
||||
),
|
||||
size: 150,
|
||||
}),
|
||||
columnHelper.accessor('description', {
|
||||
header: 'Description',
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return (
|
||||
<EditableCell
|
||||
idea={info.row.original}
|
||||
field="description"
|
||||
value={value}
|
||||
renderDisplay={(val) => {
|
||||
if (!val) return '—';
|
||||
return val.length > 80 ? `${val.slice(0, 80)}...` : val;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
size: 200,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: (info) => (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onDelete(info.row.original.id)}
|
||||
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
size: 50,
|
||||
}),
|
||||
];
|
||||
1
frontend/src/components/IdeasTable/index.ts
Normal file
1
frontend/src/components/IdeasTable/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { IdeasTable } from './IdeasTable';
|
||||
Reference in New Issue
Block a user