@nextsparkjs/theme-productivity 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/about.md +123 -0
- package/components/CardDetailModal.tsx +318 -0
- package/components/KanbanBoard.tsx +612 -0
- package/components/KanbanCard.tsx +218 -0
- package/components/KanbanColumn.tsx +264 -0
- package/components/SortableList.tsx +46 -0
- package/components/index.ts +4 -0
- package/config/app.config.ts +172 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +357 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +256 -0
- package/config/flows.config.ts +484 -0
- package/config/permissions.config.ts +167 -0
- package/config/theme.config.ts +106 -0
- package/entities/boards/boards.config.ts +61 -0
- package/entities/boards/boards.fields.ts +154 -0
- package/entities/boards/boards.service.ts +256 -0
- package/entities/boards/boards.types.ts +57 -0
- package/entities/boards/messages/en.json +80 -0
- package/entities/boards/messages/es.json +80 -0
- package/entities/boards/migrations/001_boards_table.sql +83 -0
- package/entities/cards/cards.config.ts +61 -0
- package/entities/cards/cards.fields.ts +242 -0
- package/entities/cards/cards.service.ts +336 -0
- package/entities/cards/cards.types.ts +79 -0
- package/entities/cards/messages/en.json +114 -0
- package/entities/cards/messages/es.json +114 -0
- package/entities/cards/migrations/020_cards_table.sql +92 -0
- package/entities/lists/lists.config.ts +61 -0
- package/entities/lists/lists.fields.ts +105 -0
- package/entities/lists/lists.service.ts +252 -0
- package/entities/lists/lists.types.ts +55 -0
- package/entities/lists/messages/en.json +60 -0
- package/entities/lists/messages/es.json +60 -0
- package/entities/lists/migrations/010_lists_table.sql +79 -0
- package/lib/selectors.ts +206 -0
- package/messages/en.json +79 -0
- package/messages/es.json +79 -0
- package/migrations/999_theme_sample_data.sql +922 -0
- package/migrations/999a_initial_sample_data.sql +377 -0
- package/migrations/999b_abundant_sample_data.sql +346 -0
- package/package.json +17 -0
- package/permissions-matrix.md +122 -0
- package/styles/components.css +460 -0
- package/styles/globals.css +560 -0
- package/templates/dashboard/(main)/boards/[id]/[cardId]/page.tsx +238 -0
- package/templates/dashboard/(main)/boards/[id]/edit/page.tsx +390 -0
- package/templates/dashboard/(main)/boards/[id]/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/create/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/page.tsx +335 -0
- package/templates/dashboard/(main)/layout.tsx +32 -0
- package/templates/dashboard/(main)/page.tsx +592 -0
- package/templates/shared/ProductivityMobileNav.tsx +410 -0
- package/templates/shared/ProductivitySidebar.tsx +538 -0
- package/templates/shared/ProductivityTopBar.tsx +317 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Productivity Theme
|
|
2
|
+
|
|
3
|
+
## Activación
|
|
4
|
+
|
|
5
|
+
Para usar este theme, configura en `.env`:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
NEXT_PUBLIC_ACTIVE_THEME=productivity
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Luego regenera el registry y reinicia el servidor:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx tsx scripts/build-registry.mjs --build
|
|
15
|
+
pnpm dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Funcionalidades
|
|
19
|
+
|
|
20
|
+
### Kanban Board
|
|
21
|
+
|
|
22
|
+
Vista de tablero estilo Trello con:
|
|
23
|
+
|
|
24
|
+
- **Boards**: Tableros de proyectos
|
|
25
|
+
- **Lists**: Columnas dentro de cada tablero
|
|
26
|
+
- **Cards**: Tarjetas/tareas arrastrables
|
|
27
|
+
|
|
28
|
+
### Drag and Drop
|
|
29
|
+
|
|
30
|
+
Implementado con `@dnd-kit`:
|
|
31
|
+
- Arrastrar cards entre lists
|
|
32
|
+
- Reordenar cards dentro de una list
|
|
33
|
+
- Visual feedback durante el drag
|
|
34
|
+
|
|
35
|
+
### Componentes
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
components/
|
|
39
|
+
├── KanbanBoard.tsx # Tablero completo con DnD context
|
|
40
|
+
├── KanbanColumn.tsx # Columna con drop zone
|
|
41
|
+
├── KanbanCard.tsx # Tarjeta draggable
|
|
42
|
+
└── index.ts # Exports
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Templates Override
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
templates/
|
|
49
|
+
└── app/dashboard/(main)/boards/
|
|
50
|
+
├── page.tsx # Grid de boards
|
|
51
|
+
└── [id]/
|
|
52
|
+
└── page.tsx # Vista Kanban del board
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Permisos
|
|
56
|
+
|
|
57
|
+
| Entidad | create | read | update | delete | archive |
|
|
58
|
+
|---------|:------:|:----:|:------:|:------:|:-------:|
|
|
59
|
+
| boards | owner, admin | todos | owner, admin | owner | owner, admin |
|
|
60
|
+
| lists | owner, admin | todos | owner, admin | owner, admin | - |
|
|
61
|
+
| cards | todos | todos | todos | owner, admin | - |
|
|
62
|
+
|
|
63
|
+
## Usuarios de Prueba
|
|
64
|
+
|
|
65
|
+
| Email | Password | Rol en Product Team |
|
|
66
|
+
|-------|----------|---------------------|
|
|
67
|
+
| pm.torres@nextspark.dev | Testing1234 | owner |
|
|
68
|
+
| dev.luna@nextspark.dev | Testing1234 | admin |
|
|
69
|
+
| design.rios@nextspark.dev | Testing1234 | member |
|
|
70
|
+
|
|
71
|
+
## Modo
|
|
72
|
+
|
|
73
|
+
- **Teams Mode**: `collaborative`
|
|
74
|
+
- Team switcher habilitado
|
|
75
|
+
- Puede crear equipos de trabajo
|
|
76
|
+
|
package/about.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Productivity Theme
|
|
2
|
+
|
|
3
|
+
## Objetivo
|
|
4
|
+
|
|
5
|
+
Demostrar el modo `multi-tenant` donde usuarios pueden gestionar múltiples workspaces con diferentes roles en cada team, similar a Trello o Notion.
|
|
6
|
+
|
|
7
|
+
## Producto
|
|
8
|
+
|
|
9
|
+
**TaskFlow** - Aplicación de gestión de tareas estilo Trello para equipos pequeños y medianos.
|
|
10
|
+
|
|
11
|
+
## Empresa
|
|
12
|
+
|
|
13
|
+
**FlowWorks Labs** - Empresa de software de productividad que cree en la colaboración flexible y la organización visual del trabajo.
|
|
14
|
+
|
|
15
|
+
## Teams Mode
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
multi-tenant
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Múltiples work teams (workspaces)
|
|
22
|
+
- Team switcher habilitado
|
|
23
|
+
- Puede crear nuevos teams
|
|
24
|
+
- Invitaciones habilitadas (owner/admin pueden invitar)
|
|
25
|
+
|
|
26
|
+
## Entidades
|
|
27
|
+
|
|
28
|
+
| Entidad | Descripción |
|
|
29
|
+
|---------|-------------|
|
|
30
|
+
| boards | Tableros de proyectos con nombre, descripción, color y estado |
|
|
31
|
+
| lists | Columnas dentro de tableros para organizar tarjetas |
|
|
32
|
+
| cards | Tareas individuales con título, descripción, prioridad y fechas |
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
| Feature | Descripción | Roles |
|
|
37
|
+
|---------|-------------|-------|
|
|
38
|
+
| boards.archive | Archivar tableros completados | owner, admin |
|
|
39
|
+
| cards.move | Mover tarjetas entre listas | owner, admin, member |
|
|
40
|
+
|
|
41
|
+
## Permisos
|
|
42
|
+
|
|
43
|
+
### boards
|
|
44
|
+
|
|
45
|
+
| Action | owner | admin | member |
|
|
46
|
+
|--------|:-----:|:-----:|:------:|
|
|
47
|
+
| create | ✅ | ✅ | ❌ |
|
|
48
|
+
| read | ✅ | ✅ | ✅ |
|
|
49
|
+
| update | ✅ | ✅ | ❌ |
|
|
50
|
+
| delete | ✅ | ❌ | ❌ |
|
|
51
|
+
| archive | ✅ | ✅ | ❌ |
|
|
52
|
+
|
|
53
|
+
### lists
|
|
54
|
+
|
|
55
|
+
| Action | owner | admin | member |
|
|
56
|
+
|--------|:-----:|:-----:|:------:|
|
|
57
|
+
| create | ✅ | ✅ | ❌ |
|
|
58
|
+
| read | ✅ | ✅ | ✅ |
|
|
59
|
+
| update | ✅ | ✅ | ❌ |
|
|
60
|
+
| delete | ✅ | ✅ | ❌ |
|
|
61
|
+
|
|
62
|
+
### cards
|
|
63
|
+
|
|
64
|
+
| Action | owner | admin | member |
|
|
65
|
+
|--------|:-----:|:-----:|:------:|
|
|
66
|
+
| create | ✅ | ✅ | ✅ |
|
|
67
|
+
| read | ✅ | ✅ | ✅ |
|
|
68
|
+
| update | ✅ | ✅ | ✅ |
|
|
69
|
+
| delete | ✅ | ✅ | ❌ |
|
|
70
|
+
| move | ✅ | ✅ | ✅ |
|
|
71
|
+
|
|
72
|
+
## Usuarios de Prueba
|
|
73
|
+
|
|
74
|
+
| Email | Password | Product Team | Marketing Hub |
|
|
75
|
+
|-------|----------|--------------|---------------|
|
|
76
|
+
| prod_owner_patricia@nextspark.dev | Test1234 | owner | owner |
|
|
77
|
+
| prod_admin_member_lucas@nextspark.dev | Test1234 | admin | member |
|
|
78
|
+
| prod_member_diana@nextspark.dev | Test1234 | member | - |
|
|
79
|
+
| prod_member_marcos@nextspark.dev | Test1234 | - | member |
|
|
80
|
+
|
|
81
|
+
## Billing
|
|
82
|
+
|
|
83
|
+
### Modelo de Pricing: Suscripción por Team (Flat Rate)
|
|
84
|
+
|
|
85
|
+
> **Los planes y facturas siempre están asociados al team. El precio es fijo independiente del número de miembros (hasta el límite del plan).**
|
|
86
|
+
|
|
87
|
+
### Planes Disponibles
|
|
88
|
+
|
|
89
|
+
| Plan | Mensual | Anual | Descuento |
|
|
90
|
+
|------|---------|-------|-----------|
|
|
91
|
+
| Free | $0 | $0 | - |
|
|
92
|
+
| Team | $12/mes | $115/año | ~20% off |
|
|
93
|
+
| Business | $24/mes | $230/año | ~20% off |
|
|
94
|
+
|
|
95
|
+
### Características por Plan
|
|
96
|
+
|
|
97
|
+
| Feature | Free | Team | Business |
|
|
98
|
+
|---------|:----:|:----:|:--------:|
|
|
99
|
+
| Boards | 1 | Ilimitados | Ilimitados |
|
|
100
|
+
| Miembros | 3 | 10 | Ilimitados |
|
|
101
|
+
| Listas por board | 5 | Ilimitadas | Ilimitadas |
|
|
102
|
+
| Archivos adjuntos | ❌ | 10MB/archivo | 100MB/archivo |
|
|
103
|
+
| Integraciones | ❌ | ❌ | ✅ |
|
|
104
|
+
| Admin controls | ❌ | ❌ | ✅ |
|
|
105
|
+
|
|
106
|
+
### Facturación
|
|
107
|
+
|
|
108
|
+
- **Unidad de cobro:** Por team (flat rate, no per-seat)
|
|
109
|
+
- **Ciclos:** Mensual o anual (20% descuento)
|
|
110
|
+
|
|
111
|
+
### Sample Invoices
|
|
112
|
+
|
|
113
|
+
| Team | Plan | Invoices | Status | Total |
|
|
114
|
+
|------|------|----------|--------|-------|
|
|
115
|
+
| Product Team | Team | 6 | 5 paid + 1 pending | $72 |
|
|
116
|
+
| Marketing Hub | Team | 6 | 5 paid + 1 pending | $72 |
|
|
117
|
+
|
|
118
|
+
## Casos de Uso
|
|
119
|
+
|
|
120
|
+
1. Equipo de desarrollo gestionando sprints
|
|
121
|
+
2. Agencia creativa organizando proyectos
|
|
122
|
+
3. Startup coordinando tareas entre departamentos
|
|
123
|
+
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@nextsparkjs/core/components/ui/dialog'
|
|
10
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
11
|
+
import { Input } from '@nextsparkjs/core/components/ui/input'
|
|
12
|
+
import { Textarea } from '@nextsparkjs/core/components/ui/textarea'
|
|
13
|
+
import { Label } from '@nextsparkjs/core/components/ui/label'
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@nextsparkjs/core/components/ui/select'
|
|
21
|
+
import { Calendar } from '@nextsparkjs/core/components/ui/calendar'
|
|
22
|
+
import {
|
|
23
|
+
Popover,
|
|
24
|
+
PopoverContent,
|
|
25
|
+
PopoverTrigger,
|
|
26
|
+
} from '@nextsparkjs/core/components/ui/popover'
|
|
27
|
+
import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
|
|
28
|
+
import { Calendar as CalendarIcon, Trash2, AlertCircle } from 'lucide-react'
|
|
29
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
30
|
+
import { format } from 'date-fns'
|
|
31
|
+
import type { CardData } from './KanbanCard'
|
|
32
|
+
|
|
33
|
+
interface CardDetailModalProps {
|
|
34
|
+
card: CardData | null
|
|
35
|
+
isOpen: boolean
|
|
36
|
+
onClose: () => void
|
|
37
|
+
onUpdate: (card: CardData) => Promise<void>
|
|
38
|
+
onDelete?: (cardId: string) => Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const priorityOptions = [
|
|
42
|
+
{ value: 'low', label: 'Low', color: 'bg-slate-100 text-slate-700' },
|
|
43
|
+
{ value: 'medium', label: 'Medium', color: 'bg-blue-100 text-blue-700' },
|
|
44
|
+
{ value: 'high', label: 'High', color: 'bg-orange-100 text-orange-700' },
|
|
45
|
+
{ value: 'urgent', label: 'Urgent', color: 'bg-red-100 text-red-700' },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function CardDetailModal({
|
|
49
|
+
card,
|
|
50
|
+
isOpen,
|
|
51
|
+
onClose,
|
|
52
|
+
onUpdate,
|
|
53
|
+
onDelete,
|
|
54
|
+
}: CardDetailModalProps) {
|
|
55
|
+
const [title, setTitle] = useState('')
|
|
56
|
+
const [description, setDescription] = useState('')
|
|
57
|
+
const [priority, setPriority] = useState<string>('')
|
|
58
|
+
const [dueDate, setDueDate] = useState<Date | undefined>(undefined)
|
|
59
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
60
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
61
|
+
const [hasChanges, setHasChanges] = useState(false)
|
|
62
|
+
|
|
63
|
+
// Initialize form when card changes
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (card) {
|
|
66
|
+
setTitle(card.title)
|
|
67
|
+
setDescription(card.description || '')
|
|
68
|
+
setPriority(card.priority || '')
|
|
69
|
+
setDueDate(card.dueDate ? new Date(card.dueDate) : undefined)
|
|
70
|
+
setHasChanges(false)
|
|
71
|
+
}
|
|
72
|
+
}, [card])
|
|
73
|
+
|
|
74
|
+
// Track changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!card) return
|
|
77
|
+
const changed =
|
|
78
|
+
title !== card.title ||
|
|
79
|
+
description !== (card.description || '') ||
|
|
80
|
+
priority !== (card.priority || '') ||
|
|
81
|
+
(dueDate?.toISOString().split('T')[0] || '') !== (card.dueDate || '')
|
|
82
|
+
setHasChanges(changed)
|
|
83
|
+
}, [card, title, description, priority, dueDate])
|
|
84
|
+
|
|
85
|
+
const handleSave = useCallback(async () => {
|
|
86
|
+
if (!card || !title.trim()) return
|
|
87
|
+
|
|
88
|
+
setIsSaving(true)
|
|
89
|
+
try {
|
|
90
|
+
await onUpdate({
|
|
91
|
+
...card,
|
|
92
|
+
title: title.trim(),
|
|
93
|
+
description: description.trim() || null,
|
|
94
|
+
priority: (priority as CardData['priority']) || null,
|
|
95
|
+
dueDate: dueDate ? dueDate.toISOString().split('T')[0] : null,
|
|
96
|
+
})
|
|
97
|
+
onClose()
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Error saving card:', error)
|
|
100
|
+
} finally {
|
|
101
|
+
setIsSaving(false)
|
|
102
|
+
}
|
|
103
|
+
}, [card, title, description, priority, dueDate, onUpdate, onClose])
|
|
104
|
+
|
|
105
|
+
const handleDelete = useCallback(async () => {
|
|
106
|
+
if (!card || !onDelete) return
|
|
107
|
+
if (!confirm('Are you sure you want to delete this card?')) return
|
|
108
|
+
|
|
109
|
+
setIsDeleting(true)
|
|
110
|
+
try {
|
|
111
|
+
await onDelete(card.id)
|
|
112
|
+
onClose()
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error deleting card:', error)
|
|
115
|
+
} finally {
|
|
116
|
+
setIsDeleting(false)
|
|
117
|
+
}
|
|
118
|
+
}, [card, onDelete, onClose])
|
|
119
|
+
|
|
120
|
+
// Handle keyboard shortcuts
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
123
|
+
if (!isOpen) return
|
|
124
|
+
|
|
125
|
+
// Escape to close (if no changes)
|
|
126
|
+
if (e.key === 'Escape' && !hasChanges) {
|
|
127
|
+
onClose()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cmd/Ctrl + Enter to save
|
|
131
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && hasChanges) {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
handleSave()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
138
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
139
|
+
}, [isOpen, hasChanges, onClose, handleSave])
|
|
140
|
+
|
|
141
|
+
if (!card) return null
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
145
|
+
<DialogContent className="sm:max-w-[500px]" data-cy="cards-modal">
|
|
146
|
+
<DialogHeader>
|
|
147
|
+
<DialogTitle className="sr-only">Edit Card</DialogTitle>
|
|
148
|
+
</DialogHeader>
|
|
149
|
+
|
|
150
|
+
<div className="space-y-4">
|
|
151
|
+
{/* Title */}
|
|
152
|
+
<PermissionGate permission="cards.update" fallback={
|
|
153
|
+
<div>
|
|
154
|
+
<Label className="text-muted-foreground text-xs">Title</Label>
|
|
155
|
+
<p className="font-medium mt-1">{card.title}</p>
|
|
156
|
+
</div>
|
|
157
|
+
}>
|
|
158
|
+
<div>
|
|
159
|
+
<Label htmlFor="title">Title</Label>
|
|
160
|
+
<Input
|
|
161
|
+
id="title"
|
|
162
|
+
value={title}
|
|
163
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
164
|
+
placeholder="Card title..."
|
|
165
|
+
className="mt-1"
|
|
166
|
+
autoFocus
|
|
167
|
+
data-cy="cards-modal-title"
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</PermissionGate>
|
|
171
|
+
|
|
172
|
+
{/* Description */}
|
|
173
|
+
<PermissionGate permission="cards.update" fallback={
|
|
174
|
+
<div>
|
|
175
|
+
<Label className="text-muted-foreground text-xs">Description</Label>
|
|
176
|
+
<p className="text-sm mt-1 text-muted-foreground">
|
|
177
|
+
{card.description || 'No description'}
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
}>
|
|
181
|
+
<div>
|
|
182
|
+
<Label htmlFor="description">Description</Label>
|
|
183
|
+
<Textarea
|
|
184
|
+
id="description"
|
|
185
|
+
value={description}
|
|
186
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
187
|
+
placeholder="Add a more detailed description..."
|
|
188
|
+
className="mt-1 min-h-[100px]"
|
|
189
|
+
data-cy="cards-modal-description"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</PermissionGate>
|
|
193
|
+
|
|
194
|
+
{/* Priority and Due Date Row */}
|
|
195
|
+
<div className="grid grid-cols-2 gap-4">
|
|
196
|
+
{/* Priority */}
|
|
197
|
+
<PermissionGate permission="cards.update" fallback={
|
|
198
|
+
<div>
|
|
199
|
+
<Label className="text-muted-foreground text-xs">Priority</Label>
|
|
200
|
+
<p className="text-sm mt-1">{card.priority || 'None'}</p>
|
|
201
|
+
</div>
|
|
202
|
+
}>
|
|
203
|
+
<div>
|
|
204
|
+
<Label>Priority</Label>
|
|
205
|
+
<Select value={priority} onValueChange={setPriority}>
|
|
206
|
+
<SelectTrigger className="mt-1" data-cy="cards-modal-priority">
|
|
207
|
+
<SelectValue placeholder="Select priority..." />
|
|
208
|
+
</SelectTrigger>
|
|
209
|
+
<SelectContent>
|
|
210
|
+
<SelectItem value="none">None</SelectItem>
|
|
211
|
+
{priorityOptions.map((opt) => (
|
|
212
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
213
|
+
<span className={cn('px-2 py-0.5 rounded text-xs', opt.color)}>
|
|
214
|
+
{opt.label}
|
|
215
|
+
</span>
|
|
216
|
+
</SelectItem>
|
|
217
|
+
))}
|
|
218
|
+
</SelectContent>
|
|
219
|
+
</Select>
|
|
220
|
+
</div>
|
|
221
|
+
</PermissionGate>
|
|
222
|
+
|
|
223
|
+
{/* Due Date */}
|
|
224
|
+
<PermissionGate permission="cards.update" fallback={
|
|
225
|
+
<div>
|
|
226
|
+
<Label className="text-muted-foreground text-xs">Due Date</Label>
|
|
227
|
+
<p className="text-sm mt-1">
|
|
228
|
+
{card.dueDate ? format(new Date(card.dueDate), 'PPP') : 'None'}
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
}>
|
|
232
|
+
<div>
|
|
233
|
+
<Label>Due Date</Label>
|
|
234
|
+
<Popover>
|
|
235
|
+
<PopoverTrigger asChild>
|
|
236
|
+
<Button
|
|
237
|
+
variant="outline"
|
|
238
|
+
className={cn(
|
|
239
|
+
'w-full mt-1 justify-start text-left font-normal',
|
|
240
|
+
!dueDate && 'text-muted-foreground'
|
|
241
|
+
)}
|
|
242
|
+
data-cy="cards-modal-due-date"
|
|
243
|
+
>
|
|
244
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
245
|
+
{dueDate ? format(dueDate, 'PPP') : 'Pick a date'}
|
|
246
|
+
</Button>
|
|
247
|
+
</PopoverTrigger>
|
|
248
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
249
|
+
<Calendar
|
|
250
|
+
mode="single"
|
|
251
|
+
selected={dueDate}
|
|
252
|
+
onSelect={setDueDate}
|
|
253
|
+
initialFocus
|
|
254
|
+
/>
|
|
255
|
+
{dueDate && (
|
|
256
|
+
<div className="p-2 border-t">
|
|
257
|
+
<Button
|
|
258
|
+
variant="ghost"
|
|
259
|
+
size="sm"
|
|
260
|
+
className="w-full"
|
|
261
|
+
onClick={() => setDueDate(undefined)}
|
|
262
|
+
>
|
|
263
|
+
Clear date
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</PopoverContent>
|
|
268
|
+
</Popover>
|
|
269
|
+
</div>
|
|
270
|
+
</PermissionGate>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Actions */}
|
|
274
|
+
<div className="flex items-center justify-between pt-4 border-t">
|
|
275
|
+
<PermissionGate permission="cards.delete">
|
|
276
|
+
<Button
|
|
277
|
+
variant="destructive"
|
|
278
|
+
size="sm"
|
|
279
|
+
onClick={handleDelete}
|
|
280
|
+
disabled={isDeleting}
|
|
281
|
+
data-cy="cards-modal-delete"
|
|
282
|
+
>
|
|
283
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
284
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
285
|
+
</Button>
|
|
286
|
+
</PermissionGate>
|
|
287
|
+
|
|
288
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
289
|
+
{hasChanges && (
|
|
290
|
+
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
|
291
|
+
<AlertCircle className="h-3 w-3" />
|
|
292
|
+
Unsaved changes
|
|
293
|
+
</span>
|
|
294
|
+
)}
|
|
295
|
+
<Button variant="outline" onClick={onClose} disabled={isSaving} data-cy="cards-modal-cancel">
|
|
296
|
+
Cancel
|
|
297
|
+
</Button>
|
|
298
|
+
<PermissionGate permission="cards.update">
|
|
299
|
+
<Button
|
|
300
|
+
onClick={handleSave}
|
|
301
|
+
disabled={!hasChanges || isSaving || !title.trim()}
|
|
302
|
+
data-cy="cards-modal-save"
|
|
303
|
+
>
|
|
304
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
305
|
+
</Button>
|
|
306
|
+
</PermissionGate>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{/* Keyboard hint */}
|
|
311
|
+
<p className="text-xs text-muted-foreground text-center">
|
|
312
|
+
Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-xs">⌘ Enter</kbd> to save
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
</DialogContent>
|
|
316
|
+
</Dialog>
|
|
317
|
+
)
|
|
318
|
+
}
|