feat: editing cafe data
This commit is contained in:
parent
c0b8710ee5
commit
9c331470b5
6 changed files with 131 additions and 28 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { fetchMatchEvents, MatchEvent } from '../services/campaignApi'
|
import { fetchMatchEvents, MatchEvent } from '../services/campaignApi'
|
||||||
|
import { useNow } from './useNow'
|
||||||
|
|
||||||
export function useMatchEvents() {
|
export function useMatchEvents() {
|
||||||
const [matches, setMatches] = useState<MatchEvent[]>([])
|
const [matches, setMatches] = useState<MatchEvent[]>([])
|
||||||
const [now, setNow] = useState(() => new Date())
|
const now = useNow()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMatchEvents()
|
fetchMatchEvents()
|
||||||
|
|
@ -11,11 +12,6 @@ export function useMatchEvents() {
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setInterval(() => setNow(new Date()), 1000)
|
|
||||||
return () => clearInterval(timer)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// drop matches whose kickoff has already passed — only upcoming ones should ever be selectable
|
// drop matches whose kickoff has already passed — only upcoming ones should ever be selectable
|
||||||
const upcomingMatches = matches.filter((match) => new Date(match.match_datetime).getTime() > now.getTime())
|
const upcomingMatches = matches.filter((match) => new Date(match.match_datetime).getTime() > now.getTime())
|
||||||
|
|
||||||
|
|
|
||||||
12
src/hooks/useNow.ts
Normal file
12
src/hooks/useNow.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function useNow() {
|
||||||
|
const [now, setNow] = useState(() => new Date())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => setNow(new Date()), 1000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import BookIcon from '../components/BookIcon'
|
import BookIcon from '../components/BookIcon'
|
||||||
import { mockCafeEvents } from '../data/mockCafeEvents'
|
import { useNow } from '../hooks/useNow'
|
||||||
|
import { CafeSchedule, fetchCafeSchedules } from '../services/campaignApi'
|
||||||
|
import { getCafeUuid } from '../utils/cafe'
|
||||||
|
import { formatPersianNumber } from '../utils/formatNumber'
|
||||||
|
import { formatMatchLabel } from '../utils/matchCountdown'
|
||||||
|
|
||||||
function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) {
|
function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -52,6 +56,16 @@ function PanelRow({ icon, label, onClick }: { icon: ReactNode; label: string; on
|
||||||
|
|
||||||
export default function CafeOwnerPanel() {
|
export default function CafeOwnerPanel() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const now = useNow()
|
||||||
|
const [schedules, setSchedules] = useState<CafeSchedule[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cafeUuid = getCafeUuid()
|
||||||
|
if (!cafeUuid) return
|
||||||
|
fetchCafeSchedules(cafeUuid)
|
||||||
|
.then(setSchedules)
|
||||||
|
.catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white pb-20">
|
<div className="min-h-screen bg-white pb-20">
|
||||||
|
|
@ -72,23 +86,47 @@ export default function CafeOwnerPanel() {
|
||||||
<h2 className="mt-6 text-right font-semibold text-gray-900">رویدادهای فعال شما</h2>
|
<h2 className="mt-6 text-right font-semibold text-gray-900">رویدادهای فعال شما</h2>
|
||||||
|
|
||||||
<div className="mt-3 flex flex-col gap-6">
|
<div className="mt-3 flex flex-col gap-6">
|
||||||
{mockCafeEvents.map((event) => (
|
{schedules.map((schedule) => {
|
||||||
<div key={event.id} className="rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F]">
|
const isPast = new Date(schedule.match_event.match_datetime).getTime() <= now.getTime()
|
||||||
<div className="pt-4">
|
const matchLabel = formatMatchLabel(schedule.match_event, now)
|
||||||
<p className="text-right text-gray-900 pb-3">{event.matchLabel}</p>
|
|
||||||
<span className="mt-2 inline-block rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700">
|
return (
|
||||||
{event.remainingSeats} صندلی از {event.totalSeats} باقی مانده
|
<div
|
||||||
</span>
|
key={schedule.uuid}
|
||||||
|
className={`rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F] ${isPast ? 'opacity-50' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="pt-4">
|
||||||
|
<div className="flex items-center justify-between pb-3">
|
||||||
|
<p className="text-right text-gray-900">{matchLabel}</p>
|
||||||
|
{isPast && (
|
||||||
|
<span className="shrink-0 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500">برگزار شده</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="mt-2 inline-block rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700">
|
||||||
|
{formatPersianNumber(schedule.remaining_tickets)} صندلی از {formatPersianNumber(schedule.total_tickets)} باقی مانده
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PanelRow
|
||||||
|
icon={<CalendarIcon />}
|
||||||
|
label="مشاهده رزروها"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/owner/events/${schedule.uuid}/reservations`, {
|
||||||
|
state: {
|
||||||
|
event: {
|
||||||
|
id: schedule.uuid,
|
||||||
|
matchLabel,
|
||||||
|
remainingSeats: schedule.remaining_tickets,
|
||||||
|
totalSeats: schedule.total_tickets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)
|
||||||
<PanelRow
|
})}
|
||||||
icon={<CalendarIcon />}
|
|
||||||
label="مشاهده رزروها"
|
|
||||||
onClick={() => navigate(`/owner/events/${event.id}/reservations`, { state: { event } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ import {
|
||||||
City,
|
City,
|
||||||
fetchCafeFeatures,
|
fetchCafeFeatures,
|
||||||
fetchCities,
|
fetchCities,
|
||||||
|
fetchMyCafes,
|
||||||
registerCafe,
|
registerCafe,
|
||||||
registerCafeOwner,
|
registerCafeOwner,
|
||||||
|
updateCafe,
|
||||||
} from "../services/campaignApi";
|
} from "../services/campaignApi";
|
||||||
import { getCafeUuid, setCafeUuid } from "../utils/cafe";
|
import { getCafeUuid, setCafeUuid } from "../utils/cafe";
|
||||||
import { toEnglishDigits } from "../utils/formatNumber";
|
import { toEnglishDigits } from "../utils/formatNumber";
|
||||||
import { toE164 } from "../utils/phone";
|
import { fromE164, toE164 } from "../utils/phone";
|
||||||
|
|
||||||
// Tehran — default pin until the owner picks their cafe's actual location
|
// Tehran — default pin until the owner picks their cafe's actual location
|
||||||
const DEFAULT_CAFE_LOCATION = { lat: 35.6892, lng: 51.389 };
|
const DEFAULT_CAFE_LOCATION = { lat: 35.6892, lng: 51.389 };
|
||||||
|
|
@ -298,6 +300,9 @@ export default function CafeOwnerRegister() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const initialStep = (location.state as { step?: 1 | 2 } | null)?.step ?? 1;
|
const initialStep = (location.state as { step?: 1 | 2 } | null)?.step ?? 1;
|
||||||
|
// the panel's "اطلاعات کافه" link is the only place that lands here with step 2 directly —
|
||||||
|
// treat that as "editing an existing cafe" rather than "continuing a fresh registration"
|
||||||
|
const isEditMode = initialStep === 2;
|
||||||
const [step, setStep] = useState<1 | 2>(initialStep);
|
const [step, setStep] = useState<1 | 2>(initialStep);
|
||||||
|
|
||||||
const [fullName, setFullName] = useState("");
|
const [fullName, setFullName] = useState("");
|
||||||
|
|
@ -334,6 +339,22 @@ export default function CafeOwnerRegister() {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEditMode) return;
|
||||||
|
fetchMyCafes()
|
||||||
|
.then((cafes) => {
|
||||||
|
const cafe = cafes[0];
|
||||||
|
if (!cafe) return;
|
||||||
|
setCafeName(cafe.name);
|
||||||
|
setSelectedCityUuid(cafe.city.uuid);
|
||||||
|
setAddress(cafe.address);
|
||||||
|
setCafePhone(fromE164(cafe.phone_number));
|
||||||
|
setCafeLocation({ lat: cafe.location.coordinates[1], lng: cafe.location.coordinates[0] });
|
||||||
|
setSelectedFeatures(cafe.features.map((feature) => feature.uuid));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [isEditMode]);
|
||||||
|
|
||||||
const toggleFeature = (feature: string) => {
|
const toggleFeature = (feature: string) => {
|
||||||
setSelectedFeatures((prev) =>
|
setSelectedFeatures((prev) =>
|
||||||
prev.includes(feature)
|
prev.includes(feature)
|
||||||
|
|
@ -355,6 +376,17 @@ export default function CafeOwnerRegister() {
|
||||||
});
|
});
|
||||||
setOwnerUuid(owner.uuid);
|
setOwnerUuid(owner.uuid);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
|
} else if (isEditMode) {
|
||||||
|
const cafeUuid = getCafeUuid() ?? "";
|
||||||
|
await updateCafe(cafeUuid, {
|
||||||
|
city_uuid: selectedCityUuid,
|
||||||
|
name: cafeName,
|
||||||
|
phone_number: cafePhone ? toE164(cafePhone) : "",
|
||||||
|
location: { type: "Point", coordinates: [cafeLocation.lng, cafeLocation.lat] },
|
||||||
|
address,
|
||||||
|
feature_uuids: selectedFeatures,
|
||||||
|
});
|
||||||
|
navigate("/owner/panel");
|
||||||
} else {
|
} else {
|
||||||
const cafe = await registerCafe({
|
const cafe = await registerCafe({
|
||||||
owner_uuid: ownerUuid,
|
owner_uuid: ownerUuid,
|
||||||
|
|
@ -488,8 +520,8 @@ export default function CafeOwnerRegister() {
|
||||||
onClick={handleContinue}
|
onClick={handleContinue}
|
||||||
className="pointer-events-auto flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white disabled:opacity-50"
|
className="pointer-events-auto flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isSubmitting ? "در حال ارسال..." : "ادامه"}
|
{isSubmitting ? "در حال ارسال..." : isEditMode ? "ذخیره تغییرات" : "ادامه"}
|
||||||
{!isSubmitting && <ArrowIcon className="h-4 w-4" />}
|
{!isSubmitting && !isEditMode && <ArrowIcon className="h-4 w-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,20 @@ export async function registerCafe(payload: RegisterCafePayload): Promise<CafeRe
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateCafePayload {
|
||||||
|
city_uuid: string
|
||||||
|
name: string
|
||||||
|
phone_number: string
|
||||||
|
location: GeoPoint
|
||||||
|
address: string
|
||||||
|
feature_uuids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCafe(cafeUuid: string, payload: UpdateCafePayload): Promise<CafeResponse> {
|
||||||
|
const { data } = await api.patch<CafeResponse>(`/campaign/cafes/${cafeUuid}/`, payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
export interface CafeMedia {
|
export interface CafeMedia {
|
||||||
uuid: string
|
uuid: string
|
||||||
cafe_uuid: string
|
cafe_uuid: string
|
||||||
|
|
@ -129,3 +143,8 @@ export async function createCafeSchedule(cafeUuid: string, payload: CreateCafeSc
|
||||||
const { data } = await api.post<CafeSchedule>(`/campaign/cafes/${cafeUuid}/schedules/`, payload)
|
const { data } = await api.post<CafeSchedule>(`/campaign/cafes/${cafeUuid}/schedules/`, payload)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCafeSchedules(cafeUuid: string): Promise<CafeSchedule[]> {
|
||||||
|
const { data } = await api.get<CafeSchedule[]>(`/campaign/cafes/${cafeUuid}/schedules/`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { toEnglishDigits } from './formatNumber'
|
import { toEnglishDigits, toPersianDigits } from './formatNumber'
|
||||||
|
|
||||||
export function toE164(localPhone: string): string {
|
export function toE164(localPhone: string): string {
|
||||||
const digitsOnly = toEnglishDigits(localPhone).replace(/\D/g, '')
|
const digitsOnly = toEnglishDigits(localPhone).replace(/\D/g, '')
|
||||||
const withoutLeadingZero = digitsOnly.replace(/^0+/, '')
|
const withoutLeadingZero = digitsOnly.replace(/^0+/, '')
|
||||||
return `+98${withoutLeadingZero}`
|
return `+98${withoutLeadingZero}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fromE164(e164Phone: string): string {
|
||||||
|
if (!e164Phone) return ''
|
||||||
|
const local = e164Phone.startsWith('+98') ? `0${e164Phone.slice(3)}` : e164Phone
|
||||||
|
return toPersianDigits(local)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue