Add task templates API and admin management
- Add TaskTemplate model with category and frequency support - Add task template repository with CRUD and search operations - Add task template service layer - Add public API endpoints for templates (no auth required): - GET /api/tasks/templates/ - list all templates - GET /api/tasks/templates/grouped/ - templates grouped by category - GET /api/tasks/templates/search/?q= - search templates - GET /api/tasks/templates/by-category/:id/ - templates by category - GET /api/tasks/templates/:id/ - single template - Add admin panel for task template management (CRUD) - Add admin API endpoints for templates - Add seed file with predefined task templates - Add i18n translations for template errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,7 @@ export default function SettingsPage() {
|
||||
Seed Lookup Data
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Populate or refresh static lookup tables (categories, priorities, statuses, etc.)
|
||||
Populate or refresh static lookup tables and task templates
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -97,6 +97,7 @@ export default function SettingsPage() {
|
||||
<li>Task categories, priorities, statuses, frequencies</li>
|
||||
<li>Contractor specialties</li>
|
||||
<li>Subscription tiers and feature benefits</li>
|
||||
<li><strong>Task templates (60+ predefined tasks)</strong></li>
|
||||
</ul>
|
||||
Existing data will be preserved or updated.
|
||||
</AlertDialogDescription>
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { LayoutTemplate, Trash2, Search, Plus, Edit, ChevronLeft, ChevronRight, Power, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { taskTemplatesApi, lookupsApi, type TaskTemplate } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
export default function TaskTemplatesPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [params, setParams] = useState<{
|
||||
page: number;
|
||||
per_page: number;
|
||||
search: string;
|
||||
category_id?: number;
|
||||
frequency_id?: number;
|
||||
is_active?: boolean;
|
||||
}>({ page: 1, per_page: 20, search: '' });
|
||||
const [search, setSearch] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState<string>('');
|
||||
const [filterFrequency, setFilterFrequency] = useState<string>('');
|
||||
const [filterActive, setFilterActive] = useState<string>('');
|
||||
const [editingTemplate, setEditingTemplate] = useState<TaskTemplate | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
category_id: undefined as number | undefined,
|
||||
frequency_id: undefined as number | undefined,
|
||||
icon_ios: '',
|
||||
icon_android: '',
|
||||
tags: '',
|
||||
display_order: 0,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['task-templates', params],
|
||||
queryFn: () => taskTemplatesApi.list(params),
|
||||
});
|
||||
|
||||
// Fetch categories and frequencies for dropdowns
|
||||
const { data: categories } = useQuery({
|
||||
queryKey: ['lookup-categories'],
|
||||
queryFn: () => lookupsApi.categories.list(),
|
||||
});
|
||||
|
||||
const { data: frequencies } = useQuery({
|
||||
queryKey: ['lookup-frequencies'],
|
||||
queryFn: () => lookupsApi.frequencies.list(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: taskTemplatesApi.create,
|
||||
onSuccess: () => {
|
||||
toast.success('Task template created');
|
||||
queryClient.invalidateQueries({ queryKey: ['task-templates'] });
|
||||
setIsCreating(false);
|
||||
resetForm();
|
||||
},
|
||||
onError: () => toast.error('Failed to create task template'),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => taskTemplatesApi.update(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success('Task template updated');
|
||||
queryClient.invalidateQueries({ queryKey: ['task-templates'] });
|
||||
setEditingTemplate(null);
|
||||
resetForm();
|
||||
},
|
||||
onError: () => toast.error('Failed to update task template'),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: taskTemplatesApi.delete,
|
||||
onSuccess: () => {
|
||||
toast.success('Task template deleted');
|
||||
queryClient.invalidateQueries({ queryKey: ['task-templates'] });
|
||||
},
|
||||
onError: () => toast.error('Failed to delete task template'),
|
||||
});
|
||||
|
||||
const toggleActiveMutation = useMutation({
|
||||
mutationFn: taskTemplatesApi.toggleActive,
|
||||
onSuccess: () => {
|
||||
toast.success('Template status updated');
|
||||
queryClient.invalidateQueries({ queryKey: ['task-templates'] });
|
||||
},
|
||||
onError: () => toast.error('Failed to update template status'),
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
category_id: undefined,
|
||||
frequency_id: undefined,
|
||||
icon_ios: '',
|
||||
icon_android: '',
|
||||
tags: '',
|
||||
display_order: 0,
|
||||
is_active: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (template: TaskTemplate) => {
|
||||
setEditingTemplate(template);
|
||||
setFormData({
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
category_id: template.category_id ?? undefined,
|
||||
frequency_id: template.frequency_id ?? undefined,
|
||||
icon_ios: template.icon_ios,
|
||||
icon_android: template.icon_android,
|
||||
tags: template.tags,
|
||||
display_order: template.display_order,
|
||||
is_active: template.is_active,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingTemplate) {
|
||||
updateMutation.mutate({ id: editingTemplate.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const totalPages = data ? Math.ceil(data.total / params.per_page) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Task Templates</h1>
|
||||
<p className="text-muted-foreground">Manage predefined task templates for autocomplete</p>
|
||||
</div>
|
||||
<Button onClick={() => { setIsCreating(true); resetForm(); }}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="space-y-4">
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setParams({
|
||||
...params,
|
||||
search,
|
||||
page: 1,
|
||||
category_id: filterCategory && filterCategory !== 'all' ? parseInt(filterCategory) : undefined,
|
||||
frequency_id: filterFrequency && filterFrequency !== 'all' ? parseInt(filterFrequency) : undefined,
|
||||
is_active: filterActive === '' || filterActive === 'all' ? undefined : filterActive === 'true',
|
||||
});
|
||||
}} className="flex flex-wrap gap-2">
|
||||
<div className="relative flex-1 min-w-[200px]">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input placeholder="Search by title or tags..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9" />
|
||||
</div>
|
||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="All Categories" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
{categories?.map((cat) => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>{cat.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filterFrequency} onValueChange={setFilterFrequency}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="All Frequencies" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Frequencies</SelectItem>
|
||||
{frequencies?.map((freq) => (
|
||||
<SelectItem key={freq.id} value={freq.id.toString()}>{freq.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filterActive} onValueChange={setFilterActive}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="All Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="true">Active</SelectItem>
|
||||
<SelectItem value="false">Inactive</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button type="submit" variant="secondary">Apply</Button>
|
||||
{(search || (filterCategory && filterCategory !== 'all') || (filterFrequency && filterFrequency !== 'all') || (filterActive && filterActive !== 'all')) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
setFilterCategory('');
|
||||
setFilterFrequency('');
|
||||
setFilterActive('');
|
||||
setParams({ page: 1, per_page: 20, search: '' });
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">Order</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Frequency</TableHead>
|
||||
<TableHead>iOS Icon</TableHead>
|
||||
<TableHead>Active</TableHead>
|
||||
<TableHead className="w-32">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow><TableCell colSpan={7} className="text-center py-8">Loading...</TableCell></TableRow>
|
||||
) : data?.data?.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} className="text-center py-8">No task templates found. Click "Seed Lookup Data" in Settings to add templates.</TableCell></TableRow>
|
||||
) : (
|
||||
data?.data?.map((template) => (
|
||||
<TableRow key={template.id}>
|
||||
<TableCell className="text-muted-foreground">{template.display_order}</TableCell>
|
||||
<TableCell className="font-medium max-w-xs">
|
||||
<div className="truncate">{template.title}</div>
|
||||
{template.description && (
|
||||
<div className="text-xs text-muted-foreground truncate">{template.description}</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{template.category?.name || 'Uncategorized'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{template.frequency?.name || '-'}</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{template.icon_ios || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={template.is_active ? 'default' : 'secondary'}>
|
||||
{template.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => toggleActiveMutation.mutate(template.id)}
|
||||
title={template.is_active ? 'Deactivate' : 'Activate'}
|
||||
>
|
||||
<Power className={`h-4 w-4 ${template.is_active ? 'text-green-600' : 'text-muted-foreground'}`} />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleEdit(template)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="text-red-600 hover:text-red-700">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Template?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This will permanently delete "{template.title}".</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => deleteMutation.mutate(template.id)} className="bg-destructive">Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{data && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {((params.page - 1) * params.per_page) + 1} - {Math.min(params.page * params.per_page, data.total)} of {data.total}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={params.page <= 1} onClick={() => setParams({ ...params, page: params.page - 1 })}>
|
||||
<ChevronLeft className="h-4 w-4" /> Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={params.page >= totalPages} onClick={() => setParams({ ...params, page: params.page + 1 })}>
|
||||
Next <ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Dialog */}
|
||||
<Dialog open={isCreating || !!editingTemplate} onOpenChange={(open) => { if (!open) { setIsCreating(false); setEditingTemplate(null); } }}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingTemplate ? 'Edit Task Template' : 'Create Task Template'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<Label>Title *</Label>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="e.g., Clean HVAC Filter"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Detailed instructions for this task..."
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Category</Label>
|
||||
<Select
|
||||
value={formData.category_id?.toString() ?? ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, category_id: value ? parseInt(value) : undefined })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories?.map((cat) => (
|
||||
<SelectItem key={cat.id} value={cat.id.toString()}>{cat.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Frequency</Label>
|
||||
<Select
|
||||
value={formData.frequency_id?.toString() ?? ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, frequency_id: value ? parseInt(value) : undefined })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{frequencies?.map((freq) => (
|
||||
<SelectItem key={freq.id} value={freq.id.toString()}>{freq.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>iOS Icon (SF Symbol)</Label>
|
||||
<Input
|
||||
value={formData.icon_ios}
|
||||
onChange={(e) => setFormData({ ...formData, icon_ios: e.target.value })}
|
||||
placeholder="e.g., wrench.fill"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Android Icon (Material)</Label>
|
||||
<Input
|
||||
value={formData.icon_android}
|
||||
onChange={(e) => setFormData({ ...formData, icon_android: e.target.value })}
|
||||
placeholder="e.g., Build"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Tags (comma-separated)</Label>
|
||||
<Input
|
||||
value={formData.tags}
|
||||
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||
placeholder="e.g., hvac, filter, maintenance"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Display Order</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.display_order}
|
||||
onChange={(e) => setFormData({ ...formData, display_order: parseInt(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-6">
|
||||
<Switch
|
||||
checked={formData.is_active}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, is_active: checked })}
|
||||
/>
|
||||
<Label>Active</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { setIsCreating(false); setEditingTemplate(null); }}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={!formData.title}>
|
||||
{editingTemplate ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Share2,
|
||||
KeyRound,
|
||||
Smartphone,
|
||||
LayoutTemplate,
|
||||
} from 'lucide-react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
@@ -65,6 +66,7 @@ const limitationsItems = [
|
||||
|
||||
const settingsItems = [
|
||||
{ title: 'Lookup Tables', url: '/admin/lookups', icon: BookOpen },
|
||||
{ title: 'Task Templates', url: '/admin/task-templates', icon: LayoutTemplate },
|
||||
{ title: 'Admin Users', url: '/admin/admin-users', icon: UserCog },
|
||||
{ title: 'Settings', url: '/admin/settings', icon: Settings },
|
||||
];
|
||||
|
||||
@@ -1138,4 +1138,105 @@ export const promotionsApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// Task Template Types
|
||||
export interface TaskTemplateCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
display_order: number;
|
||||
}
|
||||
|
||||
export interface TaskTemplateFrequency {
|
||||
id: number;
|
||||
name: string;
|
||||
days: number;
|
||||
display_order: number;
|
||||
}
|
||||
|
||||
export interface TaskTemplate {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
category_id: number | null;
|
||||
category?: TaskTemplateCategory;
|
||||
frequency_id: number | null;
|
||||
frequency?: TaskTemplateFrequency;
|
||||
icon_ios: string;
|
||||
icon_android: string;
|
||||
tags: string;
|
||||
display_order: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TaskTemplateListParams {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
category_id?: number;
|
||||
frequency_id?: number;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateTaskTemplateRequest {
|
||||
title: string;
|
||||
description?: string;
|
||||
category_id?: number;
|
||||
frequency_id?: number;
|
||||
icon_ios?: string;
|
||||
icon_android?: string;
|
||||
tags?: string;
|
||||
display_order?: number;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateTaskTemplateRequest {
|
||||
title?: string;
|
||||
description?: string;
|
||||
category_id?: number | null;
|
||||
frequency_id?: number | null;
|
||||
icon_ios?: string;
|
||||
icon_android?: string;
|
||||
tags?: string;
|
||||
display_order?: number;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
// Task Templates API
|
||||
export const taskTemplatesApi = {
|
||||
list: async (params?: TaskTemplateListParams): Promise<PaginatedResponse<TaskTemplate>> => {
|
||||
const response = await api.get<PaginatedResponse<TaskTemplate>>('/task-templates', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: number): Promise<TaskTemplate> => {
|
||||
const response = await api.get<TaskTemplate>(`/task-templates/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateTaskTemplateRequest): Promise<TaskTemplate> => {
|
||||
const response = await api.post<TaskTemplate>('/task-templates', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: number, data: UpdateTaskTemplateRequest): Promise<TaskTemplate> => {
|
||||
const response = await api.put<TaskTemplate>(`/task-templates/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: number): Promise<void> => {
|
||||
await api.delete(`/task-templates/${id}`);
|
||||
},
|
||||
|
||||
toggleActive: async (id: number): Promise<TaskTemplate> => {
|
||||
const response = await api.post<TaskTemplate>(`/task-templates/${id}/toggle-active`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
Reference in New Issue
Block a user