File size: 8,757 Bytes
ce8f04a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import React, { useEffect, useState } from 'react';
import { Calendar, CheckCircle, Clock, Loader2, Plus, Trash2, Bell } from 'lucide-react';
import toast from 'react-hot-toast';
import {
    createPostAwardMilestone,
    deletePostAwardMilestone,
    getNotificationInbox,
    getPostAwardMilestones,
    ProjectMilestone,
    updatePostAwardMilestone,
} from '../../api/client';

interface Props {
    projectId: string;
    userId?: string;
}

const PostAwardPanel: React.FC<Props> = ({ projectId, userId }) => {
    const [milestones, setMilestones] = useState<ProjectMilestone[]>([]);
    const [loading, setLoading] = useState(true);
    const [saving, setSaving] = useState(false);
    const [title, setTitle] = useState('');
    const [dueDate, setDueDate] = useState('');
    const [description, setDescription] = useState('');
    const [alerts, setAlerts] = useState<any[]>([]);

    const load = async () => {
        setLoading(true);
        try {
            const [ms, inbox] = await Promise.all([
                getPostAwardMilestones(projectId),
                getNotificationInbox(userId, true).catch(() => ({ notifications: [] })),
            ]);
            setMilestones(ms.milestones || []);
            const related = (inbox.notifications || []).filter(
                (n: any) => n.project_id === projectId && n.type === 'post_award_deadline',
            );
            setAlerts(related);
        } catch {
            toast.error('Nie udało się załadować kamieni milowych.');
        } finally {
            setLoading(false);
        }
    };

    useEffect(() => {
        if (projectId) load();
    }, [projectId]);

    const handleAdd = async () => {
        if (!title.trim() || !dueDate) {
            toast.error('Podaj tytuł i termin.');
            return;
        }
        setSaving(true);
        try {
            await createPostAwardMilestone(projectId, {
                title: title.trim(),
                due_date: new Date(dueDate).toISOString(),
                description: description.trim() || undefined,
            });
            setTitle('');
            setDueDate('');
            setDescription('');
            toast.success('Kamień milowy dodany.');
            await load();
        } catch (e: any) {
            toast.error(e.response?.data?.detail || 'Błąd zapisu.');
        } finally {
            setSaving(false);
        }
    };

    const toggleComplete = async (m: ProjectMilestone) => {
        try {
            await updatePostAwardMilestone(projectId, m.id, {
                status: m.status === 'completed' ? 'pending' : 'completed',
            });
            await load();
        } catch {
            toast.error('Nie udało się zaktualizować statusu.');
        }
    };

    const handleDelete = async (id: string) => {
        try {
            await deletePostAwardMilestone(projectId, id);
            toast.success('Usunięto.');
            await load();
        } catch {
            toast.error('Nie udało się usunąć.');
        }
    };

    if (loading) {
        return (
            <div style={{ padding: '3rem', textAlign: 'center', color: 'var(--text-secondary)' }}>
                <Loader2 className="animate-spin" size={32} style={{ margin: '0 auto' }} />
            </div>
        );
    }

    return (
        <div style={{ maxWidth: '900px', margin: '0 auto', padding: '2rem' }}>
            <h2 style={{ fontSize: '1.6rem', fontWeight: 800, marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                <Calendar size={22} color="var(--accent-blue)" /> Post-award — śledzenie terminów
            </h2>
            <p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>
                Kamienie milowe po przyznaniu dotacji (raporty, rozliczenia). Scheduler tworzy alerty w skrzynce powiadomień.
            </p>

            {alerts.length > 0 && (
                <div className="glass-card" style={{ marginBottom: '1.5rem', padding: '1rem', borderLeft: '3px solid #f59e0b' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem', color: '#f59e0b', fontWeight: 700 }}>
                        <Bell size={16} /> Zbliżające się terminy ({alerts.length})
                    </div>
                    {alerts.map((a) => (
                        <div key={a.id} style={{ fontSize: '0.9rem', color: 'var(--text-secondary)', marginBottom: '0.25rem' }}>
                            {a.title} — {a.deadline ? new Date(a.deadline).toLocaleDateString('pl-PL') : ''}
                        </div>
                    ))}
                </div>
            )}

            <div className="glass-card" style={{ padding: '1.5rem', marginBottom: '2rem' }}>
                <h3 style={{ marginBottom: '1rem', fontSize: '1rem' }}>Nowy kamień milowy</h3>
                <div style={{ display: 'grid', gap: '0.75rem' }}>
                    <input
                        placeholder="np. Raport okresowy M1"
                        value={title}
                        onChange={(e) => setTitle(e.target.value)}
                        style={{ padding: '0.6rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.2)', color: '#fff' }}
                    />
                    <input
                        type="datetime-local"
                        value={dueDate}
                        onChange={(e) => setDueDate(e.target.value)}
                        style={{ padding: '0.6rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.2)', color: '#fff' }}
                    />
                    <textarea
                        placeholder="Opis (opcjonalnie)"
                        value={description}
                        onChange={(e) => setDescription(e.target.value)}
                        rows={2}
                        style={{ padding: '0.6rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(0,0,0,0.2)', color: '#fff' }}
                    />
                    <button className="btn btn-primary" disabled={saving} onClick={handleAdd} style={{ alignSelf: 'flex-start', display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
                        {saving ? <Loader2 size={16} className="animate-spin" /> : <Plus size={16} />} Dodaj
                    </button>
                </div>
            </div>

            <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
                {milestones.length === 0 ? (
                    <p style={{ color: 'var(--text-muted)' }}>Brak zdefiniowanych kamieni milowych.</p>
                ) : (
                    milestones.map((m) => (
                        <div key={m.id} className="glass-card" style={{ padding: '1rem 1.25rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
                            <div>
                                <div style={{ fontWeight: 700, color: m.status === 'completed' ? 'var(--accent-green)' : '#fff', textDecoration: m.status === 'completed' ? 'line-through' : 'none' }}>
                                    {m.title}
                                </div>
                                {m.description && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>{m.description}</div>}
                                <div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.35rem', display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
                                    <Clock size={13} /> Termin: {new Date(m.due_date).toLocaleString('pl-PL')}
                                </div>
                            </div>
                            <div style={{ display: 'flex', gap: '0.5rem' }}>
                                <button onClick={() => toggleComplete(m)} title="Oznacz jako ukończone" style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: m.status === 'completed' ? 'var(--accent-green)' : 'var(--text-secondary)' }}>
                                    <CheckCircle size={20} />
                                </button>
                                <button onClick={() => handleDelete(m.id)} title="Usuń" style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#ef4444' }}>
                                    <Trash2 size={18} />
                                </button>
                            </div>
                        </div>
                    ))
                )}
            </div>
        </div>
    );
};

export default PostAwardPanel;