Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 161 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import OnboardingFlow from './components/onboarding/OnboardingFlow';
import AvailabilityModal from './components/modals/AvailabilityModal';
import ConfirmationDialog from './components/modals/ConfirmationDialog';
import CreatePackageModal from './components/modals/CreatePackageModal';
import PackagePurchasesModal from './components/modals/PackagePurchasesModal';
import LessonDetailModal from './components/modals/LessonDetailModal';
import LoginPage from './components/auth/LoginPage';
import { useCoachSchedule } from './hooks/useCoachSchedule';
import { useCoachStudents } from './hooks/useCoachStudents';
import useCoachProfile from './hooks/useCoachProfile';
import useAuth from './hooks/useAuth.jsx';
import { createDefaultProfile } from './constants/profile';
import { listCoachPackages } from './api/CoachApi/packages';
import {
deleteCoachPackage,
listCoachPackages,
updateCoachPackage
} from './api/CoachApi/packages';
import {
addCoachCustomLocation,
deleteCoachLocation,
Expand Down Expand Up @@ -67,6 +72,20 @@ const resolvePackagesFromPayload = (payload) => {
return [];
};

const resolvePackageFromPayload = (payload) => {
if (!payload || typeof payload !== 'object') {
return null;
}

return payload.package || payload.data?.package || payload.data || payload.result || payload.item || payload;
};

const getApiErrorMessage = (errorBody, fallbackMessage) =>
errorBody?.message ||
errorBody?.error ||
errorBody?.errors?.[0] ||
fallbackMessage;

const defaultProfile = createDefaultProfile();

const formatDuration = (duration) => {
Expand Down Expand Up @@ -149,6 +168,8 @@ function App() {
const [visibleCalendarDates, setVisibleCalendarDates] = useState([]);
const [showAddLessonModal, setShowAddLessonModal] = useState(false);
const [showCreatePackageModal, setShowCreatePackageModal] = useState(false);
const [selectedPackageForEdit, setSelectedPackageForEdit] = useState(null);
const [selectedPackageForPurchases, setSelectedPackageForPurchases] = useState(null);
const [showLessonDetailModal, setShowLessonDetailModal] = useState(false);
const [showCreateLessonModal, setShowCreateLessonModal] = useState(false);
const [showLessonConfirmedSheet, setShowLessonConfirmedSheet] = useState(false);
Expand Down Expand Up @@ -1395,6 +1416,125 @@ function App() {
packagesFetchedRef.current = true;
};

const handlePackageUpdated = useCallback((updatedPackage, packageId, fallbackChanges = {}) => {
setProfileData((previousProfile) => {
const previousPackages = Array.isArray(previousProfile.packages)
? previousProfile.packages
: [];

return {
...previousProfile,
packages: previousPackages.map((existingPackage) => {
const existingId = existingPackage?.id ?? existingPackage?.package_id;

if (String(existingId) !== String(packageId)) {
return existingPackage;
}

if (updatedPackage && typeof updatedPackage === 'object') {
return {
...existingPackage,
...updatedPackage
};
}

return {
...existingPackage,
...fallbackChanges
};
})
};
});

setPackagesError(null);
packagesFetchedRef.current = true;
}, []);

const handlePackageArchiveToggle = useCallback(async (packageId, isActive) => {
try {
const response = await updateCoachPackage(packageId, { isActive });

if (!response) {
return { ok: false, error: 'Your session has expired. Please sign in again.' };
}

if (!response.ok) {
let message = 'Failed to update package. Please try again.';

try {
const errorBody = await response.json();
message = getApiErrorMessage(errorBody, message);
} catch {
// Ignore JSON parse errors.
}

return { ok: false, error: message };
}

const payload = await response.json().catch(() => null);
const updatedPackage = resolvePackageFromPayload(payload);

handlePackageUpdated(updatedPackage, packageId, { is_active: isActive, isActive });
return { ok: true };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : 'Failed to update package.'
};
}
}, [handlePackageUpdated]);

const handlePackageDelete = useCallback(async (packageId) => {
try {
const response = await deleteCoachPackage(packageId);

if (!response) {
return { ok: false, error: 'Your session has expired. Please sign in again.' };
}

if (!response.ok) {
let message = 'Failed to delete package. Please try again.';

try {
const errorBody = await response.json();
message = getApiErrorMessage(errorBody, message);
} catch {
// Ignore JSON parse errors.
}

return { ok: false, error: message };
}

setProfileData((previousProfile) => {
const previousPackages = Array.isArray(previousProfile.packages)
? previousProfile.packages
: [];

return {
...previousProfile,
packages: previousPackages.filter((existingPackage) => {
const existingId = existingPackage?.id ?? existingPackage?.package_id;
return String(existingId) !== String(packageId);
})
};
});

setPackagesError(null);
packagesFetchedRef.current = true;
return { ok: true };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : 'Failed to delete package.'
};
}
}, []);

const handlePackageModalClose = useCallback(() => {
setShowCreatePackageModal(false);
setSelectedPackageForEdit(null);
}, []);

const handleOnboardingComplete = async (data) => {
try {
const result = await saveProfile(data);
Expand Down Expand Up @@ -1674,7 +1814,15 @@ function App() {
onEmptySlotSelect={handleEmptySlotSelect}
onOpenAddAvailability={handleAddAvailabilityOpen}
onOpenCreateLesson={handleCreateLessonOpen}
onOpenCreatePackage={() => setShowCreatePackageModal(true)}
onOpenCreatePackage={() => {
setSelectedPackageForEdit(null);
setShowCreatePackageModal(true);
}}
onEditPackage={(lessonPackage) => {
setSelectedPackageForEdit(lessonPackage);
setShowCreatePackageModal(true);
}}
onViewPackagePurchases={(lessonPackage) => setSelectedPackageForPurchases(lessonPackage)}
onRequestAvailabilityOnboarding={handleRequestAvailabilityOnboarding}
onOpenSettings={() => navigate('/settings')}
onOpenNotifications={() => navigate('/notifications')}
Expand All @@ -1688,6 +1836,8 @@ function App() {
packagesLoading={packagesLoading}
packagesError={packagesError}
onRefreshPackages={refreshPackages}
onTogglePackageActive={handlePackageArchiveToggle}
onDeletePackage={handlePackageDelete}
locationsData={coachLocations}
locationsLoading={locationsLoading}
locationsError={locationsError}
Expand Down Expand Up @@ -1768,8 +1918,16 @@ function App() {

<CreatePackageModal
isOpen={showCreatePackageModal}
onClose={() => setShowCreatePackageModal(false)}
onClose={handlePackageModalClose}
onCreated={handlePackageCreated}
onUpdated={handlePackageUpdated}
packageToEdit={selectedPackageForEdit}
/>

<PackagePurchasesModal
isOpen={Boolean(selectedPackageForPurchases)}
onClose={() => setSelectedPackageForPurchases(null)}
lessonPackage={selectedPackageForPurchases}
/>

<AvailabilityModal
Expand Down
48 changes: 47 additions & 1 deletion src/api/CoachApi/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,57 @@ import { apiRequest } from '../apiRequest';
const buildPackagesPath = (includeInactive) =>
includeInactive ? '/coach/packages?includeInactive=true' : '/coach/packages';

const buildPackagePath = (id) => `/coach/packages/${id}`;

export const listCoachPackages = async ({ includeInactive = false } = {}) =>
apiRequest(buildPackagesPath(includeInactive), {
method: 'GET'
});

export const createCoachPackage = async (payload) =>
apiRequest('/coach/packages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

export const updateCoachPackage = async (id, payload) =>
apiRequest(buildPackagePath(id), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

export const deleteCoachPackage = async (id) =>
apiRequest(buildPackagePath(id), {
method: 'DELETE'
});

export const getCoachPackagePurchases = async (id, params = {}) => {
const query = new URLSearchParams();

Object.entries(params).forEach(([key, value]) => {
if (value === null || value === undefined || value === '') {
return;
}

query.set(key, String(value));
});

const queryString = query.toString();
const path = queryString
? `${buildPackagePath(id)}/purchases?${queryString}`
: `${buildPackagePath(id)}/purchases`;

return apiRequest(path, {
method: 'GET'
});
};

export default {
listCoachPackages
createCoachPackage,
listCoachPackages,
updateCoachPackage,
deleteCoachPackage,
getCoachPackagePurchases
};
18 changes: 17 additions & 1 deletion src/components/dashboard/DashboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ const DashboardPage = ({
onEmptySlotSelect,
onOpenAddAvailability,
onOpenCreatePackage,
onEditPackage = () => {},
onViewPackagePurchases = () => {},
onOpenCreateLesson,
onRequestAvailabilityOnboarding,
onOpenSettings,
Expand All @@ -421,6 +423,8 @@ const DashboardPage = ({
packagesLoading = false,
packagesError = null,
onRefreshPackages = () => {},
onTogglePackageActive = async () => ({ ok: false }),
onDeletePackage = async () => ({ ok: false }),
locationsData = [],
locationsLoading = false,
locationsError = null,
Expand Down Expand Up @@ -492,7 +496,15 @@ const DashboardPage = ({
? totalPrice / lessonCount
: null,
lessonTypes,
isActive: entry.is_active ?? entry.isActive ?? true
isActive: entry.is_active ?? entry.isActive ?? true,
hasPurchaseHistory: Boolean(
entry.has_purchase_history ?? entry.hasPurchaseHistory ?? false
),
purchaseCount: parseNumber(entry.purchase_count ?? entry.purchaseCount) ?? 0,
lastPurchasedAt:
entry.last_purchased_at ??
entry.lastPurchasedAt ??
null
};
})
.sort((a, b) => {
Expand Down Expand Up @@ -1554,6 +1566,10 @@ const DashboardPage = ({
packagesError={packagesError}
onRefreshPackages={onRefreshPackages}
onOpenCreatePackage={onOpenCreatePackage}
onEditPackage={onEditPackage}
onViewPackagePurchases={onViewPackagePurchases}
onTogglePackageActive={onTogglePackageActive}
onDeletePackage={onDeletePackage}
currencyFormatter={currencyFormatter}
formatLessonTypeLabel={formatLessonTypeLabel}
formatValidityLabel={formatValidityLabel}
Expand Down
Loading
Loading