Rename plugins to automations and fix scheduler/payment bugs

Major changes:
- Rename "plugins" to "automations" throughout codebase
- Move automation system to dedicated app (scheduling/automations/)
- Add new automation marketplace, creation, and management pages

Bug fixes:
- Fix payment endpoint 500 errors (use has_feature() instead of attribute)
- Fix scheduler showing "0 AM" instead of "12 AM"
- Fix scheduler business hours double-inversion display issue
- Fix scheduler scroll-to-current-time when switching views
- Fix week view centering on wrong day (use Sunday-based indexing)
- Fix capacity widget overflow with many resources
- Fix Recharts minWidth/minHeight console warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 11:56:01 -05:00
parent c333010620
commit cfb626b595
62 changed files with 4914 additions and 2413 deletions

View File

@@ -4,7 +4,7 @@ import axios from '../api/client';
import { Zap, Plus, Trash2, Clock, CheckCircle2, XCircle, ChevronDown, Power } from 'lucide-react';
import toast from 'react-hot-toast';
interface PluginInstallation {
interface AutomationInstallation {
id: string;
template_name: string;
template_description: string;
@@ -12,14 +12,14 @@ interface PluginInstallation {
logo_url?: string;
}
interface EventPlugin {
interface EventAutomation {
id: string;
event: number;
plugin_installation: number;
plugin_name: string;
plugin_description: string;
plugin_category: string;
plugin_logo_url?: string;
automation_installation: number;
automation_name: string;
automation_description: string;
automation_category: string;
automation_logo_url?: string;
trigger: string;
trigger_display: string;
offset_minutes: number;
@@ -64,41 +64,41 @@ const OFFSET_PRESETS: OffsetPreset[] = [
const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact = false }) => {
const queryClient = useQueryClient();
const [showAddForm, setShowAddForm] = useState(false);
const [selectedPlugin, setSelectedPlugin] = useState<string>('');
const [selectedAutomation, setSelectedAutomation] = useState<string>('');
const [selectedTrigger, setSelectedTrigger] = useState<string>('at_start');
const [selectedOffset, setSelectedOffset] = useState<number>(0);
// Fetch installed plugins
const { data: plugins = [] } = useQuery<PluginInstallation[]>({
queryKey: ['plugin-installations'],
// Fetch installed automations
const { data: automations = [] } = useQuery<AutomationInstallation[]>({
queryKey: ['automation-installations'],
queryFn: async () => {
const { data } = await axios.get('/plugin-installations/');
const { data } = await axios.get('/automation-installations/');
return data;
},
});
// Fetch event plugins
const { data: eventPlugins = [], isLoading } = useQuery<EventPlugin[]>({
queryKey: ['event-plugins', eventId],
// Fetch event automations
const { data: eventAutomations = [], isLoading } = useQuery<EventAutomation[]>({
queryKey: ['event-automations', eventId],
queryFn: async () => {
const { data } = await axios.get(`/event-plugins/?event_id=${eventId}`);
const { data } = await axios.get(`/event-automations/?event_id=${eventId}`);
return data;
},
enabled: !!eventId,
});
// Add plugin mutation
// Add automation mutation
const addMutation = useMutation({
mutationFn: async (data: { plugin_installation: string; trigger: string; offset_minutes: number }) => {
return axios.post('/event-plugins/', {
mutationFn: async (data: { automation_installation: string; trigger: string; offset_minutes: number }) => {
return axios.post('/event-automations/', {
event: eventId,
...data,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-plugins', eventId] });
queryClient.invalidateQueries({ queryKey: ['event-automations', eventId] });
setShowAddForm(false);
setSelectedPlugin('');
setSelectedAutomation('');
setSelectedTrigger('at_start');
setSelectedOffset(0);
toast.success('Automation added');
@@ -110,29 +110,29 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
// Toggle mutation
const toggleMutation = useMutation({
mutationFn: async (pluginId: string) => {
return axios.post(`/event-plugins/${pluginId}/toggle/`);
mutationFn: async (automationId: string) => {
return axios.post(`/event-automations/${automationId}/toggle/`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-plugins', eventId] });
queryClient.invalidateQueries({ queryKey: ['event-automations', eventId] });
},
});
// Delete mutation
const deleteMutation = useMutation({
mutationFn: async (pluginId: string) => {
return axios.delete(`/event-plugins/${pluginId}/`);
mutationFn: async (automationId: string) => {
return axios.delete(`/event-automations/${automationId}/`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-plugins', eventId] });
queryClient.invalidateQueries({ queryKey: ['event-automations', eventId] });
toast.success('Automation removed');
},
});
const handleAdd = () => {
if (!selectedPlugin) return;
if (!selectedAutomation) return;
addMutation.mutate({
plugin_installation: selectedPlugin,
automation_installation: selectedAutomation,
trigger: selectedTrigger,
offset_minutes: selectedOffset,
});
@@ -157,13 +157,13 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
<span className="text-sm font-medium text-gray-900 dark:text-white">
Automations
</span>
{eventPlugins.length > 0 && (
{eventAutomations.length > 0 && (
<span className="px-1.5 py-0.5 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded">
{eventPlugins.length}
{eventAutomations.length}
</span>
)}
</div>
{!showAddForm && plugins.length > 0 && (
{!showAddForm && automations.length > 0 && (
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-1 px-2 py-1 text-xs text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded transition-colors"
@@ -175,9 +175,9 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
</div>
{/* Existing automations */}
{eventPlugins.length > 0 && (
{eventAutomations.length > 0 && (
<div className="space-y-2">
{eventPlugins.map((ep) => (
{eventAutomations.map((ep) => (
<div
key={ep.id}
className={`flex items-center justify-between p-2 rounded-lg border ${
@@ -192,7 +192,7 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{ep.plugin_name}
{ep.automation_name}
</p>
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
<Clock className="w-3 h-3" />
@@ -229,20 +229,20 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
{/* Add automation form */}
{showAddForm && (
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800 space-y-3">
{/* Plugin selector */}
{/* Automation selector */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Plugin
Automation
</label>
<select
value={selectedPlugin}
onChange={(e) => setSelectedPlugin(e.target.value)}
value={selectedAutomation}
onChange={(e) => setSelectedAutomation(e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">Select a plugin...</option>
{plugins.map((p) => (
<option key={p.id} value={p.id}>
{p.template_name}
<option value="">Select an automation...</option>
{automations.map((a) => (
<option key={a.id} value={a.id}>
{a.template_name}
</option>
))}
</select>
@@ -302,7 +302,7 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
)}
{/* Preview */}
{selectedPlugin && (
{selectedAutomation && (
<div className="text-xs text-purple-800 dark:text-purple-200 bg-purple-100 dark:bg-purple-900/40 px-2 py-1.5 rounded">
Will run: <strong>
{selectedTrigger === 'on_complete' ? 'When completed' :
@@ -328,7 +328,7 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
</button>
<button
onClick={handleAdd}
disabled={!selectedPlugin || addMutation.isPending}
disabled={!selectedAutomation || addMutation.isPending}
className="px-3 py-1.5 text-xs bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{addMutation.isPending ? 'Adding...' : 'Add Automation'}
@@ -338,9 +338,9 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
)}
{/* Empty state */}
{eventPlugins.length === 0 && !showAddForm && (
{eventAutomations.length === 0 && !showAddForm && (
<div className="text-center py-3 text-gray-500 dark:text-gray-400">
{plugins.length > 0 ? (
{automations.length > 0 ? (
<button
onClick={() => setShowAddForm(true)}
className="text-xs text-purple-600 dark:text-purple-400 hover:underline"
@@ -349,7 +349,7 @@ const EventAutomations: React.FC<EventAutomationsProps> = ({ eventId, compact =
</button>
) : (
<p className="text-xs">
Install plugins from the Marketplace to use automations
Install automations from the Marketplace to use them
</p>
)}
</div>