diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index b651081e..5d00e84d 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -187,6 +187,26 @@ class ChannelGroupViewSet(viewsets.ModelViewSet):
except KeyError:
return [Authenticated()]
+ def destroy(self, request, *args, **kwargs):
+ """Override destroy to check for associations before deletion"""
+ instance = self.get_object()
+
+ # Check if group has associated channels
+ if instance.channels.exists():
+ return Response(
+ {"error": "Cannot delete group with associated channels"},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Check if group has M3U account associations
+ if hasattr(instance, 'm3u_account') and instance.m3u_account.exists():
+ return Response(
+ {"error": "Cannot delete group with M3U account associations"},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ return super().destroy(request, *args, **kwargs)
+
# ─────────────────────────────────────────────────────────
# 3) Channel Management (CRUD)
diff --git a/frontend/src/api.js b/frontend/src/api.js
index d75edfa9..ff95f634 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -277,6 +277,22 @@ export default class API {
}
}
+ static async deleteChannelGroup(id) {
+ try {
+ await request(`${host}/api/channels/groups/${id}/`, {
+ method: 'DELETE',
+ });
+
+ // Remove from store after successful deletion
+ useChannelsStore.getState().removeChannelGroup(id);
+
+ return true;
+ } catch (e) {
+ errorNotification('Failed to delete channel group', e);
+ throw e;
+ }
+ }
+
static async addChannel(channel) {
try {
let body = null;
diff --git a/frontend/src/components/forms/GroupManager.jsx b/frontend/src/components/forms/GroupManager.jsx
new file mode 100644
index 00000000..7709416f
--- /dev/null
+++ b/frontend/src/components/forms/GroupManager.jsx
@@ -0,0 +1,350 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Modal,
+ Stack,
+ Group,
+ Text,
+ TextInput,
+ Button,
+ ActionIcon,
+ Flex,
+ Badge,
+ Alert,
+ Divider,
+ ScrollArea,
+} from '@mantine/core';
+import {
+ SquarePlus,
+ SquarePen,
+ Trash2,
+ Check,
+ X,
+ AlertCircle,
+ Database,
+ Tv
+} from 'lucide-react';
+import { notifications } from '@mantine/notifications';
+import useChannelsStore from '../../store/channels';
+import API from '../../api';
+
+const GroupManager = ({ isOpen, onClose }) => {
+ const channelGroups = useChannelsStore((s) => s.channelGroups);
+ const [editingGroup, setEditingGroup] = useState(null);
+ const [editName, setEditName] = useState('');
+ const [newGroupName, setNewGroupName] = useState('');
+ const [isCreating, setIsCreating] = useState(false);
+ const [groupUsage, setGroupUsage] = useState({});
+ const [loading, setLoading] = useState(false);
+
+ // Fetch group usage information when modal opens
+ useEffect(() => {
+ if (isOpen) {
+ fetchGroupUsage();
+ }
+ }, [isOpen]);
+
+ const fetchGroupUsage = async () => {
+ setLoading(true);
+ try {
+ // This would ideally be a dedicated API endpoint, but we'll use the existing data
+ // For now, we'll determine usage based on the group having associated data
+ const usage = {};
+
+ // Check which groups have channels or M3U associations
+ // This is a simplified check - in a real implementation you'd want a dedicated API
+ Object.values(channelGroups).forEach(group => {
+ usage[group.id] = {
+ hasChannels: false, // Would need API call to check
+ hasM3UAccounts: false, // Would need API call to check
+ canEdit: true, // Assume editable unless proven otherwise
+ canDelete: true // Assume deletable unless proven otherwise
+ };
+ });
+
+ setGroupUsage(usage);
+ } catch (error) {
+ console.error('Error fetching group usage:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEdit = (group) => {
+ setEditingGroup(group.id);
+ setEditName(group.name);
+ };
+
+ const handleSaveEdit = async () => {
+ if (!editName.trim()) {
+ notifications.show({
+ title: 'Error',
+ message: 'Group name cannot be empty',
+ color: 'red',
+ });
+ return;
+ }
+
+ try {
+ await API.updateChannelGroup({
+ id: editingGroup,
+ name: editName.trim(),
+ });
+
+ notifications.show({
+ title: 'Success',
+ message: 'Group updated successfully',
+ color: 'green',
+ });
+
+ setEditingGroup(null);
+ setEditName('');
+ } catch (error) {
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to update group',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleCancelEdit = () => {
+ setEditingGroup(null);
+ setEditName('');
+ };
+
+ const handleCreate = async () => {
+ if (!newGroupName.trim()) {
+ notifications.show({
+ title: 'Error',
+ message: 'Group name cannot be empty',
+ color: 'red',
+ });
+ return;
+ }
+
+ try {
+ await API.addChannelGroup({
+ name: newGroupName.trim(),
+ });
+
+ notifications.show({
+ title: 'Success',
+ message: 'Group created successfully',
+ color: 'green',
+ });
+
+ setNewGroupName('');
+ setIsCreating(false);
+ fetchGroupUsage(); // Refresh usage data
+ } catch (error) {
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to create group',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleDelete = async (group) => {
+ const usage = groupUsage[group.id];
+
+ if (usage && (!usage.canDelete || usage.hasChannels || usage.hasM3UAccounts)) {
+ notifications.show({
+ title: 'Cannot Delete',
+ message: 'This group is associated with channels or M3U accounts and cannot be deleted',
+ color: 'orange',
+ });
+ return;
+ }
+
+ try {
+ await API.deleteChannelGroup(group.id);
+
+ notifications.show({
+ title: 'Success',
+ message: 'Group deleted successfully',
+ color: 'green',
+ });
+
+ fetchGroupUsage(); // Refresh usage data
+ } catch (error) {
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to delete group',
+ color: 'red',
+ });
+ }
+ };
+
+ const getGroupBadges = (group) => {
+ const usage = groupUsage[group.id];
+ const badges = [];
+
+ if (usage?.hasChannels) {
+ badges.push(
+