Initial commit: SIBU 2.0 MISSION
This commit is contained in:
47
frontend/src/stores/auth.ts
Normal file
47
frontend/src/stores/auth.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(localStorage.getItem('auth_token'))
|
||||
const role = ref<string | null>(localStorage.getItem('user_role'))
|
||||
const userName = ref<string | null>(localStorage.getItem('user_name'))
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
const isAdmin = computed(() => role.value?.toUpperCase() === 'ADMIN')
|
||||
const isDriver = computed(() => role.value?.toUpperCase() === 'DRIVER')
|
||||
const isPromoter = computed(() => role.value?.toUpperCase() === 'PROMOTER')
|
||||
const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER')
|
||||
|
||||
function login(newToken: string, newRole: string, newName: string) {
|
||||
token.value = newToken
|
||||
role.value = newRole
|
||||
userName.value = newName
|
||||
localStorage.setItem('auth_token', newToken)
|
||||
localStorage.setItem('user_role', newRole)
|
||||
localStorage.setItem('user_name', newName)
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = null
|
||||
role.value = null
|
||||
userName.value = null
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('user_role')
|
||||
localStorage.removeItem('user_name')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
role,
|
||||
userName,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
isDriver,
|
||||
isPromoter,
|
||||
isPassenger,
|
||||
login,
|
||||
logout
|
||||
}
|
||||
})
|
||||
57
frontend/src/stores/busStop.ts
Normal file
57
frontend/src/stores/busStop.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/** Pinia store for bus stop management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { BusStop } from '@/types'
|
||||
import { busStopsService } from '@/services/busStopsService'
|
||||
|
||||
export const useBusStopStore = defineStore('busStop', () => {
|
||||
const selectedStop = ref<BusStop | null>(null)
|
||||
const busStops = ref<BusStop[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function loadBusStops(force = false) {
|
||||
if (!force && busStops.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
busStops.value = await busStopsService.getAllBusStops()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load bus stops'
|
||||
console.error('Error loading bus stops:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBusStopById(id: string) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
selectedStop.value = await busStopsService.getBusStopById(id)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load bus stop'
|
||||
console.error('Error loading bus stop:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedStop(stop: BusStop | null) {
|
||||
selectedStop.value = stop
|
||||
}
|
||||
|
||||
return {
|
||||
selectedStop,
|
||||
busStops,
|
||||
isLoading,
|
||||
error,
|
||||
loadBusStops,
|
||||
loadBusStopById,
|
||||
setSelectedStop,
|
||||
}
|
||||
})
|
||||
|
||||
65
frontend/src/stores/coupon.ts
Normal file
65
frontend/src/stores/coupon.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/** Pinia store for coupon management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Coupon } from '@/types'
|
||||
import { couponsService, type CouponFilters } from '@/services/couponsService'
|
||||
|
||||
export const useCouponStore = defineStore('coupon', () => {
|
||||
const coupons = ref<Coupon[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const myCoupons = ref<any[]>([])
|
||||
const filters = ref<CouponFilters>({})
|
||||
|
||||
async function loadCoupons(newFilters?: CouponFilters) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
if (newFilters) {
|
||||
filters.value = newFilters
|
||||
}
|
||||
try {
|
||||
coupons.value = await couponsService.getAllCoupons(filters.value)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load coupons'
|
||||
console.error('Error loading coupons:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMyCoupons() {
|
||||
try {
|
||||
myCoupons.value = await couponsService.getMyCoupons()
|
||||
} catch (e) {
|
||||
console.error('Error loading my coupons:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function claimCoupon(id: string) {
|
||||
try {
|
||||
await couponsService.claimCoupon(id)
|
||||
await loadMyCoupons()
|
||||
return true
|
||||
} catch (e: any) {
|
||||
const msg = e.response?.data?.detail || e.message
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
function setFilters(newFilters: CouponFilters) {
|
||||
filters.value = newFilters
|
||||
loadCoupons()
|
||||
}
|
||||
|
||||
return {
|
||||
coupons,
|
||||
myCoupons,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
loadCoupons,
|
||||
loadMyCoupons,
|
||||
claimCoupon,
|
||||
setFilters,
|
||||
}
|
||||
})
|
||||
114
frontend/src/stores/favorites.ts
Normal file
114
frontend/src/stores/favorites.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { apiClient } from '@/services/apiClient'
|
||||
|
||||
export interface Favorite {
|
||||
id: string
|
||||
user_id: string
|
||||
item_type: 'coupon' | 'business' | 'taxi' | 'route'
|
||||
item_id: string
|
||||
item_name?: string
|
||||
item_image?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export const useFavoritesStore = defineStore('favorites', () => {
|
||||
const favorites = ref<Favorite[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Computed
|
||||
const coupons = computed(() => favorites.value.filter(f => f.item_type === 'coupon'))
|
||||
const businesses = computed(() => favorites.value.filter(f => f.item_type === 'business'))
|
||||
const taxis = computed(() => favorites.value.filter(f => f.item_type === 'taxi'))
|
||||
const routes = computed(() => favorites.value.filter(f => f.item_type === 'route'))
|
||||
|
||||
// Actions
|
||||
async function loadFavorites() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await apiClient.get('/api/favorites')
|
||||
favorites.value = response.data
|
||||
} catch (error) {
|
||||
console.error('Error loading favorites:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addFavorite(
|
||||
itemType: 'coupon' | 'business' | 'taxi' | 'route',
|
||||
itemId: string,
|
||||
itemName?: string,
|
||||
itemImage?: string
|
||||
) {
|
||||
try {
|
||||
const response = await apiClient.post('/api/favorites', {
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
item_name: itemName,
|
||||
item_image: itemImage
|
||||
})
|
||||
favorites.value.unshift(response.data)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 400) {
|
||||
// Already favorited
|
||||
return false
|
||||
}
|
||||
console.error('Error adding favorite:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFavorite(itemType: string, itemId: string) {
|
||||
try {
|
||||
await apiClient.delete(`/api/favorites/${itemType}/${itemId}`)
|
||||
favorites.value = favorites.value.filter(
|
||||
f => !(f.item_type === itemType && f.item_id === itemId)
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error removing favorite:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFavorite(
|
||||
itemType: 'coupon' | 'business' | 'taxi' | 'route',
|
||||
itemId: string,
|
||||
itemName?: string,
|
||||
itemImage?: string
|
||||
) {
|
||||
const existing = favorites.value.find(
|
||||
f => f.item_type === itemType && f.item_id === itemId
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
await removeFavorite(itemType, itemId)
|
||||
return false
|
||||
} else {
|
||||
await addFavorite(itemType, itemId, itemName, itemImage)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function isFavorite(itemType: string, itemId: string): boolean {
|
||||
return favorites.value.some(
|
||||
f => f.item_type === itemType && f.item_id === itemId
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
favorites,
|
||||
isLoading,
|
||||
coupons,
|
||||
businesses,
|
||||
taxis,
|
||||
routes,
|
||||
loadFavorites,
|
||||
addFavorite,
|
||||
removeFavorite,
|
||||
toggleFavorite,
|
||||
isFavorite
|
||||
}
|
||||
})
|
||||
39
frontend/src/stores/map.ts
Normal file
39
frontend/src/stores/map.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/** Pinia store for map state */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { BusStop } from '@/types'
|
||||
|
||||
export const useMapStore = defineStore('map', () => {
|
||||
const markers = ref<BusStop[]>([])
|
||||
const selectedStop = ref<BusStop | null>(null)
|
||||
const center = ref({ lat: 8.4177, lng: -82.4270 }) // Panama coordinates (David/Boquete area)
|
||||
const zoom = ref(12)
|
||||
|
||||
function setMarkers(stops: BusStop[]) {
|
||||
markers.value = stops
|
||||
}
|
||||
|
||||
function setSelectedStop(stop: BusStop | null) {
|
||||
selectedStop.value = stop
|
||||
}
|
||||
|
||||
function setCenter(lat: number, lng: number) {
|
||||
center.value = { lat, lng }
|
||||
}
|
||||
|
||||
function setZoom(level: number) {
|
||||
zoom.value = level
|
||||
}
|
||||
|
||||
return {
|
||||
markers,
|
||||
selectedStop,
|
||||
center,
|
||||
zoom,
|
||||
setMarkers,
|
||||
setSelectedStop,
|
||||
setCenter,
|
||||
setZoom,
|
||||
}
|
||||
})
|
||||
|
||||
78
frontend/src/stores/route.ts
Normal file
78
frontend/src/stores/route.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/** Pinia store for route management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Route, BusStop } from '@/types'
|
||||
import { routesService } from '@/services/routesService'
|
||||
|
||||
export const useRouteStore = defineStore('route', () => {
|
||||
const selectedRouteId = ref<string | null>(null)
|
||||
const selectedRouteName = ref<string | null>(null)
|
||||
const selectedRouteStops = ref<BusStop[]>([])
|
||||
const allRoutes = ref<Route[]>([])
|
||||
const isLoadingRoutes = ref(false)
|
||||
const isLoadingStops = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const hasSelectedRoute = computed(() => selectedRouteId.value !== null && selectedRouteName.value !== null)
|
||||
|
||||
async function loadRoutes(filters?: { originCity?: string, destinationCity?: string }, force = false) {
|
||||
if (!force && !filters && allRoutes.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingRoutes.value = true
|
||||
error.value = null
|
||||
try {
|
||||
allRoutes.value = await routesService.getAllRoutes(filters)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load routes'
|
||||
console.error('Error loading routes:', e)
|
||||
} finally {
|
||||
isLoadingRoutes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRouteStops(routeId: string) {
|
||||
isLoadingStops.value = true
|
||||
error.value = null
|
||||
try {
|
||||
selectedRouteStops.value = await routesService.getRouteStops(routeId)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load route stops'
|
||||
console.error('Error loading route stops:', e)
|
||||
selectedRouteStops.value = []
|
||||
} finally {
|
||||
isLoadingStops.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectRoute(routeId: string, routeName: string) {
|
||||
if (selectedRouteId.value === routeId) return
|
||||
selectedRouteId.value = routeId
|
||||
selectedRouteName.value = routeName
|
||||
selectedRouteStops.value = [] // Clear old stops immediately
|
||||
await loadRouteStops(routeId)
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedRouteId.value = null
|
||||
selectedRouteName.value = null
|
||||
selectedRouteStops.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
selectedRouteId,
|
||||
selectedRouteName,
|
||||
selectedRouteStops,
|
||||
allRoutes,
|
||||
isLoadingRoutes,
|
||||
isLoadingStops,
|
||||
error,
|
||||
hasSelectedRoute,
|
||||
loadRoutes,
|
||||
loadRouteStops,
|
||||
selectRoute,
|
||||
clearSelection,
|
||||
}
|
||||
})
|
||||
|
||||
46
frontend/src/stores/schedule.ts
Normal file
46
frontend/src/stores/schedule.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/** Pinia store for schedule management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { BusSchedule } from '@/types'
|
||||
import { schedulesService } from '@/services/schedulesService'
|
||||
|
||||
export const useScheduleStore = defineStore('schedule', () => {
|
||||
const schedules = ref<BusSchedule[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function loadRouteSchedules(routeId: string) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
schedules.value = await schedulesService.getRouteSchedules(routeId)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
||||
console.error('Error loading schedules:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStopSchedules(stopId: string) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
schedules.value = await schedulesService.getStopSchedules(stopId)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load schedules'
|
||||
console.error('Error loading schedules:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schedules,
|
||||
isLoading,
|
||||
error,
|
||||
loadRouteSchedules,
|
||||
loadStopSchedules,
|
||||
}
|
||||
})
|
||||
|
||||
36
frontend/src/stores/shuttle.ts
Normal file
36
frontend/src/stores/shuttle.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/** Pinia store for shuttle management (Intercity/Tourism) */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Shuttle } from '@/types'
|
||||
import { shuttlesService, type ShuttleFilters } from '@/services/shuttlesService'
|
||||
|
||||
export const useShuttleStore = defineStore('shuttle', () => {
|
||||
const shuttles = ref<Shuttle[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = ref<ShuttleFilters>({})
|
||||
|
||||
async function loadShuttles(newFilters?: ShuttleFilters) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
if (newFilters) {
|
||||
filters.value = newFilters
|
||||
}
|
||||
try {
|
||||
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load shuttles'
|
||||
console.error('Error loading shuttles:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shuttles,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
loadShuttles,
|
||||
}
|
||||
})
|
||||
43
frontend/src/stores/taxi.ts
Normal file
43
frontend/src/stores/taxi.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/** Pinia store for taxi management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Taxi } from '@/types'
|
||||
import { taxisService, type TaxiFilters } from '@/services/taxisService'
|
||||
|
||||
export const useTaxiStore = defineStore('taxi', () => {
|
||||
const taxis = ref<Taxi[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = ref<TaxiFilters>({})
|
||||
|
||||
async function loadTaxis(newFilters?: TaxiFilters) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
if (newFilters) {
|
||||
filters.value = newFilters
|
||||
}
|
||||
try {
|
||||
taxis.value = await taxisService.getAllTaxis(filters.value)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load taxis'
|
||||
console.error('Error loading taxis:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setFilters(newFilters: TaxiFilters) {
|
||||
filters.value = newFilters
|
||||
loadTaxis()
|
||||
}
|
||||
|
||||
return {
|
||||
taxis,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
loadTaxis,
|
||||
setFilters,
|
||||
}
|
||||
})
|
||||
|
||||
60
frontend/src/stores/theme.ts
Normal file
60
frontend/src/stores/theme.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/** Pinia store for theme management */
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// Check localStorage first, then system preference
|
||||
const getInitialTheme = (): boolean => {
|
||||
const stored = localStorage.getItem('darkMode')
|
||||
if (stored !== null) {
|
||||
return stored === 'true'
|
||||
}
|
||||
// Check system preference
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
const isDarkMode = ref<boolean>(getInitialTheme())
|
||||
|
||||
// Apply theme to document
|
||||
function applyTheme() {
|
||||
if (isDarkMode.value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.classList.remove('light-theme')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
document.documentElement.classList.add('light-theme')
|
||||
}
|
||||
localStorage.setItem('darkMode', String(isDarkMode.value))
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
isDarkMode.value = !isDarkMode.value
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
function setDarkMode(value: boolean) {
|
||||
isDarkMode.value = value
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
// Watch for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem('darkMode')) {
|
||||
isDarkMode.value = e.matches
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
|
||||
// Apply theme on initialization
|
||||
applyTheme()
|
||||
|
||||
return {
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
setDarkMode,
|
||||
applyTheme,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user