I am raw html block.
Click edit button to change this html
import React, { useState, useMemo, useCallback, useEffect } from 'react';
// Pominięto importy Firebase, aby zachować czystą wersję demo
// --- Konfiguracja i Narzędzia ---
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
if (isNaN(date)) return dateString;
// Wymuszenie daty na dzień, miesiąc, rok, aby uniknąć błędów formatowania
return new Date(date.getTime() + date.getTimezoneOffset() * 60000).toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch (e) {
return dateString;
}
};
const formatPrice = (price) => `${price.toFixed(2)} PLN`;
const ADMIN_USER_ID = 'admin-user-biegun'; // Stały ID Admina
const PARENT_USER_ID = 'default-user-1'; // Stały ID Rodzica do danych mockowych
// --- DANE MOCKOWE (symulacja 20 uczestników z bazy) ---
const MOCK_GROUPS = [
{ id: 'g1', nazwa: 'Beeski', color: 'bg-yellow-200' },
{ id: 'g2', nazwa: 'ProKids', color: 'bg-green-200' },
{ id: 'g3', nazwa: 'SemiPRO', color: 'bg-blue-200' },
{ id: 'g4', nazwa: 'Hero', color: 'bg-red-200' },
{ id: 'g5', nazwa: 'PRO', color: 'bg-indigo-200' },
{ id: 'g6', nazwa: 'Nieprzypisane', color: 'bg-gray-200' },
];
const mockChildrenGenerator = () => {
const names = [
{ imie: 'Ignacy', nazwisko: 'Stepanik', ur: 2021, parentId: PARENT_USER_ID }, // Rodzic główny (1/2 dzieci)
{ imie: 'Franciszek', nazwisko: 'Stepanik', ur: 2023, parentId: PARENT_USER_ID }, // Rodzic główny (2/2 dzieci)
{ imie: 'Anna', nazwisko: 'Kowalska', ur: 2020, parentId: 'user-2' },
{ imie: 'Michał', nazwisko: 'Zalega', ur: 2018, parentId: 'user-3' },
{ imie: 'Kasia', nazwisko: 'Wesoła', ur: 2019, parentId: 'user-4' },
{ imie: 'Piotr', nazwisko: 'Nowak', ur: 2022, parentId: 'user-5' },
{ imie: 'Julia', nazwisko: 'Maj', ur: 2021, parentId: 'user-6' },
{ imie: 'Krzysztof', nazwisko: 'Lis', ur: 2020, parentId: 'user-7' },
{ imie: 'Ola', nazwisko: 'Wróbel', ur: 2019, parentId: 'user-8' },
{ imie: 'Bartek', nazwisko: 'Kruk', ur: 2018, parentId: 'user-9' },
{ imie: 'Weronika', nazwisko: 'Górska', ur: 2022, parentId: 'user-10' },
{ imie: 'Filip', nazwisko: 'Jankowski', ur: 2023, parentId: 'user-11' },
{ imie: 'Maja', nazwisko: 'Kowalczyk', ur: 2021, parentId: 'user-12' },
{ imie: 'Adam', nazwisko: 'Wójtowicz', ur: 2020, parentId: 'user-13' },
{ imie: 'Ewa', nazwisko: 'Zając', ur: 2019, parentId: 'user-14' },
{ imie: 'Hubert', nazwisko: 'Duda', ur: 2018, parentId: 'user-15' },
{ imie: 'Lena', nazwisko: 'Kaczmarek', ur: 2022, parentId: 'user-16' },
{ imie: 'Oskar', nazwisko: 'Mazur', ur: 2023, parentId: 'user-17' },
{ imie: 'Natalia', nazwisko: 'Szymańska', ur: 2021, parentId: 'user-18' },
{ imie: 'Robert', nazwisko: 'Wysocki', ur: 2020, parentId: 'user-19' },
];
return names.map((data, index) => {
const parentEmail = `${data.imie.toLowerCase()}.${data.nazwisko.toLowerCase()}@example.com`;
const parentPhone = `5${index + 1}0-${index + 1}00-${index + 1}00`;
const groups = MOCK_GROUPS.map(g => g.nazwa).filter(n => n !== 'Nieprzypisane');
const grupa = index % 5 === 0 && index > 4 ? 'Nieprzypisane' : groups[index % groups.length];
return {
id: `dziecko${index + 1}`,
...data,
rokUrodzenia: data.ur,
grupa: grupa,
parentEmail,
parentPhone,
};
});
};
const MOCK_CHILDREN = mockChildrenGenerator();
const MOCK_PARENT_DATA = {
[PARENT_USER_ID]: {
imie: 'Kamil',
nazwisko: 'Stepanik',
email: 'kamil.stepanik@example.com',
telefon: '500-100-200',
},
// Dodajemy dane dla pozostałych rodziców, uproszczone
'user-2': { imie: 'Jan', nazwisko: 'Kowalski', email: 'jan.kowalski@example.com', telefon: '600-200-300' },
'user-3': { imie: 'Marta', nazwisko: 'Zalega', email: 'marta.zalega@example.com', telefon: '700-300-400' },
// ... i tak dalej dla reszty
};
const MOCK_TRIPS = [
{
id: 'wyjazd1',
nazwa: 'Obóz Narciarski Tatry 2026',
opis: 'Tygodniowy wyjazd szkoleniowy w Zakopanem. Pełne szkolenie PZN. Zakwaterowanie w pensjonacie Giewont.',
dataWyjazdu: '2026-01-10',
godzinaWyjazdu: '07:00',
miejsceWyjazdu: 'Kraków, parking przy Tauron Arena',
dataPowrotu: '2026-01-17',
godzinaPowrotu: '18:00',
miejscePowrotu: 'Kraków, parking przy Tauron Arena',
platnosci: [
{ id: 'p1', tytul: 'Zaliczka', kwota: 500, termin: '2024-11-15', status: 'Oczekuje' }, // ZALEGŁA
{ id: 'p2', tytul: 'II Rata', kwota: 1500, termin: '2025-12-15', status: 'Oczekuje' },
{ id: 'p3', tytul: 'Karnet (opcjonalny)', kwota: 400, termin: '2025-12-30', status: 'Oczekuje' },
],
grupy: ['Beeski', 'ProKids', 'SemiPRO', 'PRO'],
pdfUrl: 'mock/umowa_tatry.pdf'
},
{
id: 'wyjazd2',
nazwa: 'Zimowy Trening Biegowy (Weekend)',
opis: 'Szybki wypad do Szklarskiej Poręby. Hotel Sudety.',
dataWyjazdu: '2026-02-28',
godzinaWyjazdu: '16:00',
miejsceWyjazdu: 'Katowice, Dworzec Główny PKP',
dataPowrotu: '2026-03-01',
godzinaPowrotu: '20:00',
miejscePowrotu: 'Katowice, Dworzec Główny PKP',
platnosci: [{ id: 'p4', tytul: 'Całość', kwota: 999, termin: '2026-01-30', status: 'Oczekuje' }],
grupy: ['Hero', 'PRO'],
pdfUrl: 'mock/umowa_szklarska.pdf'
},
];
// Domyślne statusy wyjazdów (ustawiamy więcej dzieci na wyjazdy)
const INITIAL_TRIP_STATUSES = {};
MOCK_CHILDREN.forEach((child, index) => {
if (index % 3 !== 0) { // Co trzecie dziecko nie jedzie
if (child.grupa === 'Beeski' || child.grupa === 'ProKids' || child.grupa === 'SemiPRO' || child.grupa === 'PRO') {
INITIAL_TRIP_STATUSES['wyjazd1'] = {
...INITIAL_TRIP_STATUSES['wyjazd1'],
[child.id]: { status: 'Jedzie' }
};
}
if (child.grupa === 'Hero' || child.grupa === 'PRO') {
INITIAL_TRIP_STATUSES['wyjazd2'] = {
...INITIAL_TRIP_STATUSES['wyjazd2'],
[child.id]: { status: 'Jedzie' }
};
}
}
});
// Ustaw Ignacego jako "Jedzie"
INITIAL_TRIP_STATUSES['wyjazd1'][MOCK_CHILDREN[0].id] = { status: 'Jedzie' };
// --- NARZĘDZIA ADMINA ---
// ADMIN: Komponent Edycji Wyjazdu
const AdminEditTripModal = ({ trip, onClose, tripsData, setTripsData, updateAllData }) => {
const [editedTrip, setEditedTrip] = useState(trip);
const [newPayment, setNewPayment] = useState({ tytul: '', kwota: 0, termin: '' });
const availableGroups = MOCK_GROUPS.map(g => g.nazwa).filter(n => n !== 'Nieprzypisane');
const handleChange = (e) => {
const { name, value } = e.target;
setEditedTrip(prev => ({ ...prev, [name]: value }));
};
const handleGroupToggle = (groupName) => {
setEditedTrip(prev => ({
...prev,
grupy: prev.grupy.includes(groupName)
? prev.grupy.filter(g => g !== groupName)
: [...prev.grupy, groupName]
}));
};
const handleNewPaymentChange = (e) => {
setNewPayment({ ...newPayment, [e.target.name]: e.target.value });
};
const addPaymentToTrip = () => {
if (newPayment.tytul && newPayment.kwota > 0 && newPayment.termin) {
setEditedTrip(prev => ({
...prev,
platnosci: [...prev.platnosci, {
...newPayment,
id: `p${prev.platnosci.length + 1}`,
kwota: parseFloat(newPayment.kwota),
status: 'Oczekuje'
}]
}));
setNewPayment({ tytul: '', kwota: 0, termin: '' });
}
};
const removePayment = (paymentId) => {
setEditedTrip(prev => ({
...prev,
platnosci: prev.platnosci.filter(p => p.id !== paymentId)
}));
};
const handlePdfUpload = (e) => {
if (e.target.files.length > 0) {
alert(`Plik PDF ${e.target.files[0].name} załadowany! (Symulacja zapisu URL)`);
setEditedTrip(prev => ({...prev, pdfUrl: `mock-url-${e.target.files[0].name}`}));
}
};
const handleSave = () => {
// Aktualizacja listy wyjazdów
const updatedTrips = tripsData.map(t => t.id === editedTrip.id ? editedTrip : t);
setTripsData(updatedTrips);
// Wymuszenie aktualizacji całego stanu aplikacji
updateAllData({ newTrips: updatedTrips });
alert(`Wyjazd "${editedTrip.nazwa}" został zaktualizowany!`);
onClose();
};
return (
Edycja Wyjazdu: {trip.nazwa}
{/* Podstawowe Info */}
{/* Daty i Miejsca */}
{/* Przypisanie Grup */}
Dostępne dla Grup (Filtrowanie)
{availableGroups.map(group => (
))}
{/* Zarządzanie Płatnościami */}
{/* Umowa PDF */}
);
};
// ADMIN: Komponent Zarządzania Wyjazdami
const AdminTripsView = ({ tripsData, setTripsData, updateAllData }) => {
const [isAdding, setIsAdding] = useState(false);
const [editingTrip, setEditingTrip] = useState(null);
const [newTrip, setNewTrip] = useState({
nazwa: '', opis: '', dataWyjazdu: '', godzinaWyjazdu: '', miejsceWyjazdu: '', dataPowrotu: '', godzinaPowrotu: '', miejscePowrotu: '', platnosci: [], grupy: [], pdfUrl: null
});
const [newPayment, setNewPayment] = useState({ tytul: '', kwota: 0, termin: '' });
const availableGroups = MOCK_GROUPS.map(g => g.nazwa).filter(n => n !== 'Nieprzypisane');
// Przekazywanie logicznych zmian do stanu
const handleNewTripChange = (e) => {
setNewTrip({ ...newTrip, [e.target.name]: e.target.value });
};
const handleNewPaymentChange = (e) => {
setNewPayment({ ...newPayment, [e.target.name]: e.target.value });
};
const handleGroupToggleNew = (groupName) => {
setNewTrip(prev => ({
...prev,
grupy: prev.grupy.includes(groupName)
? prev.grupy.filter(g => g !== groupName)
: [...prev.grupy, groupName]
}));
};
const addPaymentToTrip = () => {
if (newPayment.tytul && newPayment.kwota > 0 && newPayment.termin) {
setNewTrip(prev => ({
...prev,
platnosci: [...prev.platnosci, {
...newPayment,
id: `p${prev.platnosci.length + 1}`,
kwota: parseFloat(newPayment.kwota),
status: 'Oczekuje'
}]
}));
setNewPayment({ tytul: '', kwota: 0, termin: '' });
}
};
const handleSaveTrip = () => {
if (newTrip.nazwa && newTrip.dataWyjazdu && newTrip.dataPowrotu && newTrip.platnosci.length > 0) {
const newId = `wyjazd${tripsData.length + 1}`;
const tripToSave = { ...newTrip, id: newId };
// Używamy updateAllData, aby zaktualizować tripsData i wywołać odświeżenie płatności/umów
updateAllData({
newTrips: [...tripsData, tripToSave]
});
setIsAdding(false);
setNewTrip({ nazwa: '', opis: '', dataWyjazdu: '', godzinaWyjazdu: '', miejsceWyjazdu: '', dataPowrotu: '', godzinaPowrotu: '', miejscePowrotu: '', platnosci: [], pdfUrl: null });
alert(`Wyjazd "${tripToSave.nazwa}" został dodany! (SIMULACJA zapisu do DB)`);
} else {
alert('Wypełnij wszystkie wymagane pola i dodaj przynajmniej jedną płatność.');
}
};
const handlePdfUploadNew = (e) => {
if (e.target.files.length > 0) {
alert(`Plik PDF ${e.target.files[0].name} załadowany! (Symulacja zapisu URL)`);
setNewTrip(prev => ({...prev, pdfUrl: `mock-url-${e.target.files[0].name}`}));
}
};
return (
Zarządzanie Wyjazdami
{isAdding && (
)}
Aktywne Wyjazdy
{tripsData.map(trip => (
{trip.nazwa}
{formatDate(trip.dataWyjazdu)} - {formatDate(trip.dataPowrotu)}
Grupy: {trip.grupy.join(', ')}
))}
{editingTrip && (
setEditingTrip(null)}
tripsData={tripsData}
setTripsData={setTripsData}
updateAllData={updateAllData}
/>
)}
);
};
// ADMIN: Komponent Zarządzania Płatnościami i Uczestnikami
const AdminParticipantsView = ({ tripsData, allChildren, allPayments, setAllPayments, allGroups, allParentData, updateAllData }) => {
const [selectedTripId, setSelectedTripId] = useState(tripsData[0]?.id || null);
const [selectedGroup, setSelectedGroup] = useState('Wszystkie');
const selectedTrip = tripsData.find(t => t.id === selectedTripId);
// 1. Filtrowanie dzieci, które mają status "Jedzie" dla wybranego wyjazdu
const getTripStatus = (childId, tripId, statuses) => statuses[tripId]?.[childId]?.status;
const childrenEnrolled = allChildren.filter(child => {
const status = getTripStatus(child.id, selectedTripId, INITIAL_TRIP_STATUSES);
const isEnrolled = status === 'Jedzie' || status === 'Nie wiem';
const isGroupRelevant = selectedTrip?.grupy?.includes(child.grupa);
return isEnrolled && isGroupRelevant;
});
let enrolledChildrenFiltered = childrenEnrolled;
// 2. Filtrowanie po Grupie
if (selectedGroup !== 'Wszystkie') {
enrolledChildrenFiltered = childrenEnrolled.filter(c => c.grupa === selectedGroup);
}
// 3. Obliczenie sumy zaległości i zebranie danych rodzica
const participantList = enrolledChildrenFiltered.map(child => {
const childPayments = allPayments.filter(p => p.childId === child.id && p.wyjazdId === selectedTripId);
const totalOverdue = childPayments
.filter(p => p.status !== 'Opłacone' && new Date(p.termin) < new Date())
.reduce((sum, p) => sum + p.kwota, 0);
const parentInfo = allParentData[child.parentId] || {};
return {
...child,
totalOverdue,
parentEmail: parentInfo.email,
parentPhone: parentInfo.telefon,
};
}).sort((a, b) => b.totalOverdue - a.totalOverdue); // Sortowanie po największej zaległości
// Oznaczanie płatności jako opłaconej (Symulacja akcji Rozlicz)
const handleRozlicz = (childId) => {
const childOverduePayments = allPayments.filter(p =>
p.childId === childId &&
p.wyjazdId === selectedTripId &&
p.status !== 'Opłacone'
);
if (childOverduePayments.length === 0) {
alert(`${participantList.find(p => p.id === childId).imie} nie ma zaległych płatności.`);
return;
}
const confirm = window.confirm(`Czy na pewno chcesz oznaczyć wszystkie zaległe płatności dla ${participantList.find(p => p.id === childId).imie} (${formatPrice(participantList.find(p => p.id === childId).totalOverdue)}) jako opłacone?`);
if (confirm) {
const updatedPayments = allPayments.map(p => {
if (p.childId === childId && p.wyjazdId === selectedTripId && p.status !== 'Opłacone') {
return { ...p, status: 'Opłacone' };
}
return p;
});
setAllPayments(updatedPayments);
updateAllData({ newPayments: updatedPayments });
alert(`Płatności dla ${participantList.find(p => p.id === childId).imie} rozliczone!`);
}
};
const getGroupColor = (groupName) => MOCK_GROUPS.find(g => g.nazwa === groupName)?.color || 'bg-gray-100';
return (
Lista Uczestników & Płatności
{selectedTrip && (
Uczestnicy wyjazdu: {selectedTrip.nazwa} ({participantList.length})
| Imię Nazwisko |
Grupa |
E-mail Rodzica |
Telefon Rodzica |
Zaległość (Suma) |
Akcje |
{participantList.length === 0 ? (
| Brak uczestników spełniających kryteria. |
) : (
participantList.map(p => (
| {p.imie} {p.nazwisko} |
{p.grupa}
|
{p.parentEmail} |
{p.parentPhone} |
{p.totalOverdue > 0 ? (
{formatPrice(p.totalOverdue)}
) : (
0.00 PLN
)}
|
|
))
)}
)}
);
};
// ADMIN: Komponent Zarządzania Grupami
const AdminGroupsView = ({ allChildren, setAllChildren, allGroups, setAllGroups, updateAllData }) => {
const [newGroupName, setNewGroupName] = useState('');
const [childrenToAssign, setChildrenToAssign] = useState({});
const handleAddGroup = () => {
if (newGroupName) {
setAllGroups(prev => [...prev, { id: `grupa${prev.length + 1}`, nazwa: newGroupName, color: 'bg-indigo-200' }]);
setNewGroupName('');
}
};
const handleAssignGroup = (childId, groupName) => {
setAllChildren(prev => {
const updatedChildren = prev.map(child =>
child.id === childId ? { ...child, grupa: groupName } : child
);
updateAllData({ newChildren: updatedChildren });
return updatedChildren;
});
setChildrenToAssign({}); // Wyczyść stan przypisania po operacji
};
const groupedChildren = allChildren.reduce((acc, child) => {
const group = child.grupa || 'Nieprzypisane';
if (!acc[group]) acc[group] = [];
acc[group].push(child);
return acc;
}, {});
const unassignedChildren = groupedChildren['Nieprzypisane'] || [];
const getGroupColor = (groupName) => MOCK_GROUPS.find(g => g.nazwa === groupName)?.color || 'bg-gray-100';
return (
Zarządzanie Grupami
{/* Tworzenie Grupy */}
{/* Przypisywanie Dzieci */}
Przypisz Dzieci do Grup
{unassignedChildren.length > 0 ? (
Dzieci bez grupy ({unassignedChildren.length})
{unassignedChildren.map(child => (
{child.imie} {child.nazwisko} ({child.rokUrodzenia})
{childrenToAssign[child.id] && (
)}
))}
) : (
Wszystkie dzieci są już przypisane do grup.
)}
{/* Lista Grup z Dziećmi */}
Aktywne Grupy
{Object.entries(groupedChildren)
.filter(([groupName]) => groupName !== 'Nieprzypisane')
.map(([groupName, children]) => (
{groupName} ({children.length})
{children.map(c => {c.imie} {c.nazwisko})}
))}
);
};
// --- WIDOKI RODZICA ---
const Sidebar = ({ activeView, setActiveView, userRole, setUserRole }) => {
const isParentView = userRole === 'parent';
const menuItemsParent = [
{ id: 'start', label: 'Start', icon: (
)},
{ id: 'wyjazdy', label: 'Wyjazdy', icon: (
)},
{ id: 'platnosci', label: 'Płatności', icon: (
)},
{ id: 'umowy', label: 'Umowy', icon: (
)},
{ id: 'ustawienia', label: 'Ustawienia', icon: (
)},
];
const menuItemsAdmin = [
{ id: 'admin-trips', label: 'Zarządzaj Wyjazdami', icon: (
)},
{ id: 'admin-participants', label: 'Uczestnicy & Płatności', icon: (
)},
{ id: 'admin-groups', label: 'Zarządzaj Grupami', icon: (
)},
];
const currentMenuItems = isParentView ? menuItemsParent : menuItemsAdmin;
return (
);
};
// --- Komponent do Wyboru Aktywnego Dziecka ---
const ChildSelector = ({ childrenData, activeChildId, setActiveChildId }) => {
// Filtrujemy tylko dzieci danego rodzica
const relevantChildren = childrenData.filter(c => c.parentId === PARENT_USER_ID);
if (relevantChildren.length === 0) {
return
Brak dodanych dzieci
;
}
return (
);
};
// Widok: Ustawienia (Rodzic i Dzieci)
const SettingsView = ({ childrenData, setChildrenData, parentData, setParentData, setActiveChildId, updateAllData }) => {
const handleParentDataChange = (e) => {
setParentData(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const saveParentData = () => {
alert('Dane rodzica zostały zapisane!');
// updateAllData({ newParentData: parentData }); // opcjonalna aktualizacja w złożonym scenariuszu
};
const handleAddChild = () => {
const newId = `dziecko${childrenData.length + 1}`;
const newChild = {
id: newId,
imie: 'Nowe',
nazwisko: `Dziecko ${childrenData.length + 1}`,
rokUrodzenia: new Date().getFullYear(),
grupa: 'Nieprzypisane',
wzrost: '0 cm',
dodatkoweInfo: 'Brak',
parentId: PARENT_USER_ID, // Przypisujemy do aktywnego rodzica
parentEmail: parentData.email,
parentPhone: parentData.telefon,
};
const updatedChildren = [...childrenData, newChild];
setChildrenData(updatedChildren);
setActiveChildId(newId);
updateAllData({ newChildren: updatedChildren });
alert('Dodano nowe dziecko! Edytuj jego dane w formularzu obok.');
};
// Filtrowanie dzieci tylko dla aktywnego rodzica
const activeParentChildren = childrenData.filter(c => c.parentId === PARENT_USER_ID);
const ChildCard = ({ child }) => (
{child.imie[0]}{child.nazwisko[0]}
{child.imie} {child.nazwisko}
Rok ur.: {child.rokUrodzenia} | Grupa: {child.grupa}
Wzrost: {child.wzrost || 'N/A'} | Dodatkowe info: {child.dodatkoweInfo || 'Brak'}
);
return (
USTAWIENIA
{/* Dane Rodzica */}
{/* Dane Dzieci */}
Lista Dzieci
{activeParentChildren.length > 0 ? (
activeParentChildren.map(child =>
)
) : (
Nie masz jeszcze dodanych dzieci.
)}
);
};
// Widok: Płatności
const PaymentsView = ({ paymentsData, activeChild }) => {
const [expandedTripId, setExpandedTripId] = useState(null);
if (!activeChild || !activeChild.id) {
return
Wybierz dziecko w nagłówku.
;
}
const filteredPayments = paymentsData.filter(p => p.childId === activeChild.id);
const groupedPayments = filteredPayments.reduce((acc, payment) => {
const key = payment.wyjazdId;
if (!acc[key]) {
acc[key] = {
wyjazdNazwa: payment.wyjazdNazwa,
payments: []
};
}
acc[key].payments.push(payment);
return acc;
}, {});
const isOverdue = (dateStr) => new Date(dateStr) < new Date();
const getStatusText = (payment) => {
if (payment.status === 'Opłacone') return { text: 'Opłacone', color: 'text-blue-600', bg: 'bg-blue-100' };
if (isOverdue(payment.termin)) return { text: 'Zaległe', color: 'text-red-600', bg: 'bg-red-100' };
// W tej symulacji status 'Oczekuje' jest ustawiany, ale dla uproszczenia w UI pokazujemy zaległe i opłacone.
return { text: 'Oczekuje', color: 'text-gray-600', bg: 'bg-gray-100' };
};
return (
Płatności ({activeChild.imie} {activeChild.nazwisko})
{Object.keys(groupedPayments).length === 0 ? (
Brak aktywnych płatności dla {activeChild.imie} (Zapisz dziecko na wyjazd!).
) : (
{Object.entries(groupedPayments).map(([tripId, group]) => {
const isExpanded = expandedTripId === tripId;
return (
setExpandedTripId(isExpanded ? null : tripId)}>
{group.wyjazdNazwa}
{isExpanded && (
{group.payments.map(payment => {
const status = getStatusText(payment);
return (
{payment.tytul}
Termin: {formatDate(payment.termin)}
{formatPrice(payment.kwota)}
{status.text}
{status.text !== 'Opłacone' && (
)}
);
})}
)}
);
})}
)}
);
};
// Widok: Umowy
const AgreementsView = ({ agreementsData, activeChild, parentData, setAgreementsData, updateAllData }) => {
if (!activeChild || !activeChild.id) {
return
Wybierz dziecko w nagłówku.
;
}
const filteredAgreements = agreementsData.filter(a => a.childId === activeChild.id);
const handleSignAgreement = (agreement) => {
if (!parentData.imie || !parentData.nazwisko || !parentData.email) {
alert('UWAGA: Proszę uzupełnić Imię, Nazwisko i Email w Ustawieniach Rodzica przed podpisaniem umowy!');
return;
}
setAgreementsData(prevAgreements => {
const updatedAgreements = prevAgreements.map(a =>
a.id === agreement.id
? { ...a, status: 'Podpisana' }
: a
);
// Wymuś ponowne wygenerowanie stanów z nowym statusem umowy
updateAllData({ newAgreements: updatedAgreements });
return updatedAgreements;
});
alert(`Umowa dla "${agreement.wyjazdNazwa}" została podpisana (SYMULACJA). Właściwy PDF zostanie wysłany na e-mail: ${parentData.email}.`);
};
const handleViewAgreement = (agreement) => {
alert(`SYMULACJA: Otwieranie szablonu PDF dla umowy: "${agreement.wyjazdNazwa}".`);
};
const AgreementCard = ({ agreement, isSigned }) => (
{/* Zgodnie z życzeniem, wyświetlamy tylko tytuł wyjazdu */}
{agreement.wyjazdNazwa}
{isSigned &&
Status: Podpisana
}
{isSigned ? (
) : (
)}
);
return (
Umowy dla {activeChild.imie} {activeChild.nazwisko}
Umowy do podpisania
{filteredAgreements.filter(a => a.status !== 'Podpisana').length > 0 ? (
filteredAgreements.filter(a => a.status !== 'Podpisana').map(agreement => (
))
) : (
Brak umów oczekujących na podpisanie dla {activeChild.imie}.
)}
Umowy podpisane
{filteredAgreements.filter(a => a.status === 'Podpisana').length > 0 ? (
filteredAgreements.filter(a => a.status === 'Podpisana').map(agreement => (
))
) : (
Brak podpisanych umów dla {activeChild.imie}.
)}
);
};
// Widok: Wyjazdy
const TripsView = ({ activeChild, tripsData, userTripStatuses, setUserTripStatuses, setActiveView, updateAllData }) => {
const [expandedTripId, setExpandedTripId] = useState(null);
if (!activeChild || !activeChild.id) {
return
Wybierz dziecko w nagłówku.
;
}
const updateTripStatus = (childId, tripId, newStatus) => {
setUserTripStatuses(prev => {
const updatedStatuses = {
...prev,
[tripId]: {
...prev[tripId],
[childId]: { status: newStatus }
}
};
// Wymuś ponowne wygenerowanie płatności i umów po zmianie statusu wyjazdu
updateAllData({ newStatuses: updatedStatuses });
return updatedStatuses;
});
console.log(`SIMULACJA: Status dziecka ${childId} zmieniony na ${newStatus} dla wyjazdu ${tripId}`);
};
// Przycisk "Potwierdź" tylko dla celów wizualnych
const handleConfirmStatus = (tripId) => {
alert(`Decyzja dla wyjazdu ${tripsData.find(t => t.id === tripId)?.nazwa} została potwierdzona!`);
};
const handleViewAgreement = (trip) => {
alert(`SYMULACJA: Otwieranie szablonu PDF dla wyjazdu: "${trip.nazwa}".`);
};
const TripCard = ({ trip }) => {
const status = userTripStatuses[trip.id]?.[activeChild.id]?.status || 'Nie wiem';
const isExpanded = expandedTripId === trip.id;
const statusMap = {
'Jedzie': { label: 'Jedzie', color: 'bg-green-600 text-white', hoverColor: 'bg-green-700' },
'Nie jedzie': { label: 'Nie jedzie', color: 'bg-red-600 text-white', hoverColor: 'bg-red-700' },
'Nie wiem': { label: 'Nie wiem', color: 'bg-gray-600 text-white', hoverColor: 'bg-gray-700' },
};
const isAgreementRequired = true; // Zawsze wymagamy umowy
return (
setExpandedTripId(isExpanded ? null : trip.id)}>
{trip.nazwa}
{isExpanded && (
{trip.opis}
{/* Detale Wyjazdu i Powrotu */}
WYJAZD
Data: {formatDate(trip.dataWyjazdu)}
Godzina: {trip.godzinaWyjazdu}
Miejsce: {trip.miejsceWyjazdu}
POWRÓT
Data: {formatDate(trip.dataPowrotu)}
Godzina: {trip.godzinaPowrotu}
Miejsce: {trip.miejscePowrotu}
{/* Płatności - Skrót */}
Szczegóły płatności
{trip.platnosci.map(p => (
{p.tytul}: {p.kwota} PLN
Do: {formatDate(p.termin)}
))}
{/* Status Uczestnictwa i Umowa */}
{/* Decyzja */}
Decyzja {activeChild.imie}
{/* Przyciski Decyzji */}
{Object.entries(statusMap).map(([key, data]) => (
))}
{/* Umowa do podpisu */}
{isAgreementRequired && (
Umowa do podpisu
)}
)}
);
};
return (
Dostępne wyjazdy dla {activeChild.imie}
{tripsData.length > 0 ? (
tripsData.map(trip =>
)
) : (
Brak dostępnych wyjazdów.
)}
);
};
// Widok: Start (Dashboard)
const DashboardView = ({ activeChild, tripsData, paymentsData, agreementsData, setActiveView }) => {
if (!activeChild || !activeChild.id) {
return (
Witaj!
Aby rozpocząć, przejdź do zakładki Ustawienia i dodaj swoje dziecko, lub wybierz je w nagłówku.
);
}
const childPayments = paymentsData.filter(p => p.childId === activeChild.id);
// Filtrowanie tylko zaległych płatności
const overduePayments = childPayments.filter(p =>
p.status !== 'Opłacone' && new Date(p.termin) < new Date()
).map(p => ({
...p,
fullTitle: `${p.wyjazdNazwa} - ${p.tytul} ${p.kwota} zł`
}));
// Najbliższe wyjazdy (z kalendarza), sortowane po dacie
const upcomingTrips = tripsData
.filter(trip => new Date(trip.dataWyjazdu) >= new Date())
.sort((a, b) => new Date(a.dataWyjazdu) - new Date(b.dataPowrotu))
.slice(0, 2);
// Umowy do podpisania (dla dashboardu) - 3 najbliższe umowy (na podstawie daty wyjazdu)
const agreementsToSign = agreementsData
.filter(a => a.childId === activeChild.id && a.status !== 'Podpisana')
.map(a => ({
...a,
dataWyjazdu: tripsData.find(t => t.id === a.idWyjazdu)?.dataWyjazdu
}))
.sort((a, b) => new Date(a.dataWyjazdu) - new Date(b.dataWyjazdu))
.slice(0, 3);
return (
Witaj, {activeChild.imie} {activeChild.nazwisko}!
{/* Block 1: Najbliższe wyjazdy */}
Najbliższe wyjazdy
{upcomingTrips.length > 0 ? (
upcomingTrips.map(trip => (
{trip.nazwa}
Wyjazd: {formatDate(trip.dataWyjazdu)} | Powrót: {formatDate(trip.dataPowrotu)}
))
) : (
Brak nadchodzących wyjazdów w kalendarzu.
)}
{/* Block 2: Płatności i Umowy */}
{/* Płatności (TYLKO Zaległe) */}
Płatności
{overduePayments.length > 0 ? (
overduePayments.map(p => (
setActiveView('platnosci')}>
{p.fullTitle}
Termin minął: {formatDate(p.termin)}
))
) : (
Brak zaległych płatności.
)}
{/* Umowy do Podpisania */}
Umowy do Podpisania
{agreementsToSign.length > 0 ? (
agreementsToSign.map(a => (
setActiveView('umowy')}>
{a.wyjazdNazwa}
Wyjazd: {formatDate(a.dataWyjazdu)}
))
) : (
Brak umów oczekujących na podpis.
)}
);
};
// --- Komponent Logowania / Rejestracji ---
const AuthScreen = ({ onLogin }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isRegistering, setIsRegistering] = useState(false);
const [error, setError] = useState('');
const handleAuth = () => {
setError('');
if (!email || !password) {
setError('Wypełnij oba pola.');
return;
}
let role = null;
let userId = null;
// Symulacja Logowania
if (!isRegistering) {
if (email === 'admin@biegun.pl' && password === 'admin123') {
role = 'admin';
userId = ADMIN_USER_ID;
} else if (email === 'rodzic@biegun.pl' && password === '123456') {
role = 'parent';
userId = PARENT_USER_ID;
} else {
setError('Nieprawidłowy e-mail lub hasło. Spróbuj admin@biegun.pl/admin123 lub rodzic@biegun.pl/123456.');
return;
}
onLogin(role, userId);
} else {
// Symulacja Rejestracji - Logujemy jako domyślny rodzic dla demo
alert('Rejestracja zakończona pomyślnie! Zostaniesz zalogowany jako domyślny rodzic.');
onLogin('parent', PARENT_USER_ID);
}
};
return (
BS
BiegunSport Login
{isRegistering ? 'Utwórz nowe konto' : 'Zaloguj się do panelu rodzica/admina'}
);
};
// Główny komponent Aplikacji
const App = () => {
const [userRole, setUserRole] = useState(null); // null = niezalogowany
const [currentUserId, setCurrentUserId] = useState(null);
const [activeView, setActiveView] = useState('start');
// --- Stany Danych ---
const [childrenData, setChildrenData] = useState(MOCK_CHILDREN);
const [parentData, setParentData] = useState(MOCK_PARENT_DATA[PARENT_USER_ID]); // Ustawiamy tylko dane aktywnego rodzica
const [tripsData, setTripsData] = useState(MOCK_TRIPS);
const [allGroups, setAllGroups] = useState(MOCK_GROUPS);
const [userTripStatuses, setUserTripStatuses] = useState(INITIAL_TRIP_STATUSES);
const initialChild = MOCK_CHILDREN.find(c => c.parentId === PARENT_USER_ID) || {};
const [activeChildId, setActiveChildId] = useState(initialChild.id || null);
// Wyprowadzenie aktywnego dziecka do obiektu
const activeChild = useMemo(() => childrenData.find(c => c.id === activeChildId) || {}, [childrenData, activeChildId]);
// --- Logika Logowania/Wylogowania ---
const handleLogin = (role, userId) => {
setCurrentUserId(userId);
setUserRole(role);
setActiveView(role === 'admin' ? 'admin-trips' : 'start');
// Załaduj dane rodzica po zalogowaniu
if (role === 'parent' && MOCK_PARENT_DATA[userId]) {
setParentData(MOCK_PARENT_DATA[userId]);
const parentChildren = childrenData.filter(c => c.parentId === userId);
if (parentChildren.length > 0 && !activeChildId) {
setActiveChildId(parentChildren[0].id);
}
}
};
const handleLogout = () => {
setCurrentUserId(null);
setUserRole(null);
setActiveView('start');
setParentData({});
};
// --- Logika Umów i Płatności (Generowanie) ---
const generatePaymentsAndAgreements = useCallback((
currentChildren, currentTrips, currentStatuses, currentPayments, currentAgreements
) => {
const newPayments = [];
const agreementsToCheck = [];
currentChildren.forEach(child => {
currentTrips.forEach(trip => {
const enrollmentStatus = currentStatuses[trip.id]?.[child.id]?.status || 'Nie wiem';
const isGroupRelevant = trip.grupy?.includes(child.grupa) || false;
if (isGroupRelevant) {
if (enrollmentStatus === 'Jedzie' || enrollmentStatus === 'Nie wiem') {
// Płatności
trip.platnosci.forEach((p) => {
const existingPayment = currentPayments.find(ap => ap.wyjazdId === trip.id && ap.childId === child.id && ap.tytul === p.tytul);
const isOverdue = new Date(p.termin) < new Date();
const status = existingPayment?.status === 'Opłacone'
? 'Opłacone'
: (isOverdue ? 'Zaległe' : 'Oczekuje');
newPayments.push({
...p,
id: `${child.id}_${trip.id}_${p.id}`,
wyjazdId: trip.id,
wyjazdNazwa: trip.nazwa,
childId: child.id,
status: status
});
});
// Umowy
const existingAgreement = currentAgreements.find(a => a.idWyjazdu === trip.id && a.childId === child.id);
agreementsToCheck.push({
id: `${child.id}_${trip.id}_contract`,
nazwa: trip.nazwa,
idWyjazdu: trip.id,
childId: child.id,
wyjazdNazwa: trip.nazwa,
pdfUrl: trip.pdfUrl,
status: existingAgreement?.status === 'Podpisana' ? 'Podpisana' : 'Do podpisania'
});
}
}
});
});
return { payments: newPayments, agreements: agreementsToCheck };
}, []);
const [allPayments, setAllPayments] = useState([]);
const [agreementsData, setAgreementsData] = useState([]);
const updateAllData = useCallback(({
newChildren, newTrips, newStatuses, newPayments, newAgreements
}) => {
const currentChildren = newChildren !== undefined ? newChildren : childrenData;
const currentTrips = newTrips !== undefined ? newTrips : tripsData;
const currentStatuses = newStatuses !== undefined ? newStatuses : userTripStatuses;
const currentPayments = newPayments !== undefined ? newPayments : allPayments;
const currentAgreements = newAgreements !== undefined ? newAgreements : agreementsData;
// Ustawienie stanów, które się zmieniły
if (newChildren !== undefined) setChildrenData(newChildren);
if (newTrips !== undefined) setTripsData(newTrips);
if (newStatuses !== undefined) setUserTripStatuses(newStatuses);
if (newPayments !== undefined) setAllPayments(newPayments);
if (newAgreements !== undefined) setAgreementsData(newAgreements);
// Ponowne generowanie płatności i umów na podstawie NOWYCH stanów
const { payments, agreements } = generatePaymentsAndAgreements(
currentChildren, currentTrips, currentStatuses, currentPayments, currentAgreements
);
if (newPayments === undefined) setAllPayments(payments);
if (newAgreements === undefined) setAgreementsData(agreements);
}, [childrenData, tripsData, userTripStatuses, allPayments, agreementsData, generatePaymentsAndAgreements]);
// Inicializacja przy starcie - generowanie początkowych danych
useEffect(() => {
const { payments, agreements } = generatePaymentsAndAgreements(
MOCK_CHILDREN, MOCK_TRIPS, INITIAL_TRIP_STATUSES, [], []
);
setAllPayments(payments);
setAgreementsData(agreements);
}, [generatePaymentsAndAgreements]);
const renderView = () => {
// Ekran Logowania
if (!currentUserId) {
return
;
}
// --- WIDOKI ADMINA ---
if (userRole === 'admin') {
switch (activeView) {
case 'admin-trips':
return
;
case 'admin-participants':
return
;
case 'admin-groups':
return
;
default:
return
;
}
}
// --- WIDOKI RODZICA ---
// Jeśli rodzic jest zalogowany, ale nie ma dzieci
const parentChildren = childrenData.filter(c => c.parentId === currentUserId);
if (parentChildren.length === 0) {
if (activeView !== 'ustawienia') {
setActiveView('ustawienia');
}
// Przekazujemy aktywnemu rodzicowi dane do edycji (choć są puste)
if (!parentData || !parentData.email) {
setParentData({ imie: '', nazwisko: '', email: MOCK_PARENT_DATA[currentUserId]?.email || '', telefon: '' });
}
}
switch (activeView) {
case 'start':
return
p.childId === activeChildId)}
agreementsData={agreementsData}
setActiveView={setActiveView}
/>;
case 'wyjazdy':
return ;
case 'platnosci':
return p.childId === activeChildId)} />;
case 'umowy':
return ;
case 'ustawienia':
return ;
default:
return p.childId === activeChildId)}
agreementsData={agreementsData}
setActiveView={setActiveView}
/>;
}
};
return (
{currentUserId &&
}
{currentUserId && (
)}
{renderView()}
);
};
export default App;