replace: swap prosappweb content for prosapp_web_app (more complete version)

prosapp_web_app has chat, dashboard, calendar, support, 13 providers and
Fluro URL routing. Keep Dockerfile + nginx.conf from previous prosappweb.
Upgrade google_fonts 6.2.1 → 8.1.0 (Dart 3.12 compat fix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-18 15:26:32 -05:00
co-authored by Claude Sonnet 4.6
parent 74a4f41902
commit 15175c1b91
334 changed files with 12914 additions and 25727 deletions
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
class CustomOutlinedButton extends StatelessWidget {
final Function onPressed;
final String text;
final Color color;
final bool isFilled;
const CustomOutlinedButton({
super.key,
required this.onPressed,
required this.text,
this.color = Colors.blue,
this.isFilled = false,
});
@override
Widget build(BuildContext context) {
return OutlinedButton(
style: ButtonStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
side: WidgetStateProperty.all(
BorderSide(color: color),
),
backgroundColor: WidgetStateProperty.all(
isFilled ? color.withOpacity(0.3) : Colors.transparent,
),
),
onPressed: () => onPressed(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Text(
text,
style: const TextStyle(
fontSize: 16,
color: Colors.blue,
),
),
),
);
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
class LinkText extends StatefulWidget {
final String text;
final Function()? onPressed;
const LinkText({super.key, required this.text, this.onPressed});
@override
State<LinkText> createState() => _LinkTextState();
}
class _LinkTextState extends State<LinkText> {
bool isHover = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (widget.onPressed != null) widget.onPressed!();
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => isHover = true),
onExit: (_) => setState(() => isHover = false),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Text(
widget.text,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
decoration: isHover ? TextDecoration.underline : null,
),
),
),
),
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class WhiteCard extends StatelessWidget {
final String? title;
final Widget child;
final double? width;
const WhiteCard({super.key, this.title, required this.child, this.width});
@override
Widget build(BuildContext context) {
return Container(
width: width, // width == null ? null : width,
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(10),
decoration: buildBoxDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
FittedBox(
fit: BoxFit.contain,
child: Text(
title!,
style: GoogleFonts.roboto(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
const Divider()
],
child
],
),
);
}
BoxDecoration buildBoxDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)
],
);
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class CustomInputs {
static InputDecoration loginInputDecoration({
required String hint,
required String label,
required IconData icon,
}) {
return InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.3)),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.3)),
),
hintText: hint,
labelText: label,
prefixIcon: Icon(icon, color: Colors.grey),
labelStyle: const TextStyle(color: Colors.grey),
hintStyle: const TextStyle(color: Colors.grey),
);
}
static InputDecoration searchInputDecoration({
required String hint,
required IconData icon,
}) {
return InputDecoration(
enabledBorder: InputBorder.none,
border: InputBorder.none,
hintText: hint,
prefixIcon: Icon(icon, color: Colors.grey),
labelStyle: const TextStyle(color: Colors.grey),
hintStyle: const TextStyle(color: Colors.grey),
);
}
static InputDecoration formInputDecoration({
required String hint,
required String label,
required IconData icon,
IconButton? iconButton,
}) {
return InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.indigo.withOpacity(0.3)),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.indigo.withOpacity(0.3)),
),
hintText: hint,
labelText: label,
prefixIcon: Icon(icon, color: Colors.grey),
suffixIcon: iconButton,
labelStyle: const TextStyle(color: Colors.grey),
hintStyle: const TextStyle(color: Colors.grey),
);
}
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CustomLabels {
static TextStyle h1 = GoogleFonts.roboto(
fontSize: 30,
fontWeight: FontWeight.w400,
);
static TextStyle h2 = GoogleFonts.roboto(
fontSize: 20,
fontWeight: FontWeight.w400,
);
static TextStyle h3 = GoogleFonts.roboto(
fontSize: 18,
fontWeight: FontWeight.w400,
);
static TextStyle h4 = GoogleFonts.roboto(
fontSize: 13,
fontWeight: FontWeight.w400,
);
static TextStyle h5 = GoogleFonts.roboto(
fontSize: 15,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
);
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/layouts/auth/widgets/custom_title.dart';
import 'package:prosapp_web_app/ui/layouts/auth/widgets/background_auth.dart';
class AuthLayout extends StatelessWidget {
final Widget child;
const AuthLayout({super.key, required this.child});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
body: Scrollbar(
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
size.width > 1000
? _DesktopBody(child: child)
: _MobileBody(child: child),
],
),
),
);
}
}
class _MobileBody extends StatelessWidget {
final Widget child;
const _MobileBody({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
const CustomTitle(),
SizedBox(width: double.infinity, height: 420, child: child),
const SizedBox(
width: double.infinity,
height: 400,
child: BackgroundAuth(),
)
],
),
);
}
}
class _DesktopBody extends StatelessWidget {
final Widget child;
const _DesktopBody({super.key, required this.child});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Container(
width: size.width,
height: size.height,
color: Colors.white,
child: Row(
children: [
const Expanded(child: BackgroundAuth()),
// View container
Container(
width: 600,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
const SizedBox(height: 20),
const CustomTitle(),
const SizedBox(height: 50),
Expanded(child: child),
],
),
),
],
),
);
}
}
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class BackgroundAuth extends StatelessWidget {
const BackgroundAuth({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: const Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
child: Image(
image: AssetImage('prosapp-logo.png'),
),
),
),
),
);
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CustomTitle extends StatelessWidget {
const CustomTitle({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 100), // Añade espacio arriba
Image.asset(
'logo.png',
width: 60,
height: 60,
),
const SizedBox(height: 40), // Mantiene el espacio entre el logo y el texto
FittedBox(
fit: BoxFit.contain,
child: Text(
"Iniciar sesión",
style: GoogleFonts.montserratAlternates(
fontSize: 30,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
)
],
),
);
}
}
@@ -0,0 +1,85 @@
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/ui/shared/navbar.dart';
import 'package:prosapp_web_app/ui/shared/sidebar.dart';
import 'package:flutter/material.dart';
class DashboardLayout extends StatefulWidget {
final Widget child;
const DashboardLayout({super.key, required this.child});
@override
State<DashboardLayout> createState() => _DashboardLayoutState();
}
class _DashboardLayoutState extends State<DashboardLayout>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
SideMenuProvider.menuController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: const Color(0xffEDF1F2),
body: Stack(
children: [
Row(
children: [
if (size.width >= 700) const Sidebar(),
Expanded(
child: Column(
children: [
// Navbar
const Navbar(),
// View
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: widget.child,
),
),
],
),
)
],
),
if (size.width < 700)
AnimatedBuilder(
animation: SideMenuProvider.menuController,
builder: (context, _) => Stack(
children: [
if (SideMenuProvider.isOpen)
Opacity(
opacity: SideMenuProvider.opacity.value,
child: GestureDetector(
onTap: () => SideMenuProvider.closeMenu(),
child: Container(
width: size.width,
height: size.height,
color: Colors.black26,
),
),
),
Transform.translate(
offset: Offset(SideMenuProvider.movement.value, 0),
child: const Sidebar(),
),
],
),
),
],
),
);
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class SplashLayout extends StatelessWidget {
const SplashLayout({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('Checking...'),
],
),
),
);
}
}
+71
View File
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/score_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/ui/shared/widgets/navbar_avatar.dart';
import 'package:provider/provider.dart';
class Navbar extends StatelessWidget {
const Navbar({super.key});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final professionalProvider = Provider.of<ProfessionalProvider>(context);
final scoreProvider = Provider.of<ScoreProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((_) {
scoreProvider.loadReputation(context.read<AuthProvider>().user!.id);
});
return Consumer<ScoreProvider>(builder: (context, scoreProvider, child) {
final reputation = scoreProvider.reputation;
return Container(
width: double.infinity,
height: 50,
decoration: buildBoxDecoration(),
child: Row(
children: [
if (size.width <= 700) ...[
IconButton(
icon: const Icon(Icons.menu_outlined),
onPressed: () => SideMenuProvider.openMenu(),
),
],
const SizedBox(width: 5),
const Spacer(),
Row(
children: [
Icon(Icons.star, color: Colors.yellow[600], size: 15),
const SizedBox(width: 2),
Text(
professionalProvider.isProModeActive
? reputation.averagePro.toStringAsFixed(2)
: reputation.average.toStringAsFixed(2),
style: const TextStyle(fontSize: 15),
),
],
),
const SizedBox(width: 10),
const NavbarAvatar(),
const SizedBox(width: 10),
],
),
);
});
}
BoxDecoration buildBoxDecoration() {
return const BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 5,
),
]);
}
}
+214
View File
@@ -0,0 +1,214 @@
import 'package:prosapp_web_app/models/pro_state.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/shared/widgets/menu_item.dart';
import 'package:prosapp_web_app/ui/shared/widgets/text_separator.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:prosapp_web_app/ui/shared/widgets/logo.dart';
import 'package:provider/provider.dart';
class Sidebar extends StatelessWidget {
const Sidebar({super.key});
void navigateTo(String routeName) {
NavigationService.replaceTo(routeName);
SideMenuProvider.closeMenu();
}
@override
Widget build(BuildContext context) {
final sideMenuProvider = Provider.of<SideMenuProvider>(context);
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final professionalProvider = Provider.of<ProfessionalProvider>(context);
return Container(
width: 200,
height: double.infinity,
decoration: buildBoxDecoration(),
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
const Logo(),
const SizedBox(height: 50),
if (professionalProvider.isProModeActive) ...[
const TextSeparator(text: 'Profesional'),
MenuItem(
isActive:
sideMenuProvider.currentPage == Flurorouter.profileRoute ||
sideMenuProvider.currentPage ==
Flurorouter.professionalProfileRoute,
text: 'Perfil',
icon: Icons.person_outlined,
onPressed: () {
if (professionalProvider.isProModeActive) {
navigateTo(Flurorouter.professionalProfileRoute);
} else {
navigateTo(Flurorouter.profileRoute);
}
}
// () => navigateTo(Flurorouter.profileRoute),
),
MenuItem(
isActive: sideMenuProvider.currentPage ==
Flurorouter.professionalServicesRequestsRoute,
text: 'Mis solicitudes',
icon: Icons.room_service_outlined,
onPressed: () => {
Provider.of<ServicesProvider>(context, listen: false)
.clearServices(),
navigateTo(Flurorouter.professionalServicesRequestsRoute)
},
),
MenuItem(
isActive: sideMenuProvider.currentPage ==
Flurorouter.professionalServicesRoute,
text: 'Mis servicios',
icon: Icons.list,
onPressed: () => {
Provider.of<ServicesProvider>(context, listen: false)
.clearServices(),
navigateTo(Flurorouter.professionalServicesRoute)
},
),
MenuItem(
isActive: sideMenuProvider.currentPage ==
Flurorouter.professionalServicesHistoryRoute,
text: 'Historial',
icon: Icons.restore_outlined,
onPressed: () => {
Provider.of<ServicesProvider>(context, listen: false)
.clearServices(),
navigateTo(Flurorouter.professionalServicesHistoryRoute)
},
),
MenuItem(
text: 'Calendario',
icon: Icons.calendar_month_outlined,
onPressed: () =>
navigateTo(Flurorouter.professionalCalendarRoute),
),
] else ...[
const TextSeparator(text: 'Usuario'),
MenuItem(
isActive:
sideMenuProvider.currentPage == Flurorouter.profileRoute,
text: 'Perfil',
icon: Icons.person_outlined,
onPressed: () => navigateTo(Flurorouter.profileRoute),
),
MenuItem(
isActive:
sideMenuProvider.currentPage == Flurorouter.dashboardRoute,
text: 'Solicitar',
icon: Icons.room_service_outlined,
onPressed: () => navigateTo(Flurorouter.dashboardRoute),
),
MenuItem(
isActive:
sideMenuProvider.currentPage == Flurorouter.userServicesRoute,
text: 'Mis servicios',
icon: Icons.list,
onPressed: () => {
Provider.of<ServicesProvider>(context, listen: false)
.clearServices(),
navigateTo(Flurorouter.userServicesRoute)
},
),
MenuItem(
isActive: sideMenuProvider.currentPage ==
Flurorouter.userServicesHistoryRoute,
text: 'Historial',
icon: Icons.restore_outlined,
onPressed: () => {
Provider.of<ServicesProvider>(context, listen: false)
.clearServices(),
navigateTo(Flurorouter.userServicesHistoryRoute)
},
),
MenuItem(
isActive:
sideMenuProvider.currentPage == Flurorouter.supportRoute,
text: 'Soporte',
icon: Icons.support_agent_outlined,
onPressed: () => navigateTo(Flurorouter.supportRoute),
),
],
Material(
color: Colors.blueAccent,
child: InkWell(
onTap: () {
if (authProvider.user?.picture == '' ||
authProvider.user?.city == '' ||
authProvider.user?.name == null ||
authProvider.user?.name == '' ||
authProvider.user?.email == '') {
NotificationsService.showSnackBarError(
'Primero completa tu perfil');
navigateTo(Flurorouter.profileRoute);
return;
}
if (authProvider.user?.proState == ProState.active) {
professionalProvider.toggleProMode();
if (professionalProvider.isProModeActive) {
navigateTo(Flurorouter.professionalServicesRequestsRoute);
} else {
navigateTo(Flurorouter.dashboardRoute);
}
} else {
navigateTo(Flurorouter.requestProfessionalRoute);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 10,
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
'Modo ${professionalProvider.isProModeActive ? 'Usuario' : 'Profesional'}',
style: GoogleFonts.roboto(
color: Colors.white.withOpacity(0.8)),
),
),
),
),
),
const SizedBox(height: 50),
const TextSeparator(text: 'Salir'),
MenuItem(
text: 'Cerrar sesión',
icon: Icons.exit_to_app_outlined,
onPressed: () => authProvider.logout(),
),
],
),
);
}
BoxDecoration buildBoxDecoration() => const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xff092044),
Color(0xff092042),
],
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
),
],
);
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class Logo extends StatelessWidget {
const Logo({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('logo.png', width: 50, height: 50),
SizedBox(width: 8),
Text(
'Prosapp',
style: GoogleFonts.montserratAlternates(
fontSize: 20,
fontWeight: FontWeight.w200,
color: Colors.white,
),
),
SizedBox(width: 10),
],
),
);
}
}
+71
View File
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class MenuItem extends StatefulWidget {
final String text;
final IconData icon;
final bool isActive;
final Function onPressed;
const MenuItem({
super.key,
required this.text,
required this.icon,
this.isActive = false,
required this.onPressed,
});
@override
State<MenuItem> createState() => _MenuItemState();
}
class _MenuItemState extends State<MenuItem> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
color: isHovered
? Colors.white.withOpacity(0.1)
: widget.isActive
? Colors.white.withOpacity(0.1)
: Colors.transparent,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.isActive ? null : () => widget.onPressed(),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 10,
),
child: MouseRegion(
cursor: widget.isActive
? SystemMouseCursors.basic
: SystemMouseCursors.click,
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
widget.icon,
color: Colors.white.withOpacity(0.3),
),
const SizedBox(width: 10),
Text(
widget.text,
style: GoogleFonts.roboto(
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
),
),
),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:provider/provider.dart';
class NavbarAvatar extends StatelessWidget {
const NavbarAvatar({super.key});
@override
Widget build(BuildContext context) {
final user = Provider.of<AuthProvider>(context).user!;
final image = (user.picture == '' || user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: user.picture!,
);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => NavigationService.replaceTo(Flurorouter.profileRoute),
child: SizedBox(
width: 40,
height: 40,
child: ClipOval(child: image),
),
),
);
}
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
class ScheduleDayTile extends StatelessWidget {
final String day;
final ScheduleEntity schedule;
final Function(bool) onEnableChanged;
final Function(bool) onContinuousDayChanged;
final Function(TimeOfDay) onRange1Hour1Pick;
final Function(TimeOfDay) onRange1Hour2Pick;
final Function(TimeOfDay) onRange2Hour1Pick;
final Function(TimeOfDay) onRange2Hour2Pick;
const ScheduleDayTile({
super.key,
required this.day,
required this.schedule,
required this.onEnableChanged,
required this.onContinuousDayChanged,
required this.onRange1Hour1Pick,
required this.onRange1Hour2Pick,
required this.onRange2Hour1Pick,
required this.onRange2Hour2Pick,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
SwitchListTile(
title: Text(day),
value: schedule.enabled,
onChanged: onEnableChanged,
),
if (schedule.enabled) ...[
SwitchListTile(
title: const Text('Jornada continua'),
value: schedule.continuousDay,
onChanged: onContinuousDayChanged,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
timePickerField(
context: context,
text: ScheduleEntity.getFormatTime(schedule.range1Hour1),
onPick: onRange1Hour1Pick,
),
Center(
child: Container(
width: 10,
height: 1,
color: Colors.black,
),
),
if (!schedule.continuousDay) ...[
timePickerField(
context: context,
text: ScheduleEntity.getFormatTime(schedule.range1Hour2),
onPick: onRange1Hour2Pick,
),
const SizedBox(width: 10),
timePickerField(
context: context,
text: ScheduleEntity.getFormatTime(schedule.range2Hour1),
onPick: onRange2Hour1Pick,
),
Center(
child: Container(
width: 10,
height: 1,
color: Colors.black,
),
),
],
timePickerField(
context: context,
text: ScheduleEntity.getFormatTime(schedule.range2Hour2),
onPick: onRange2Hour2Pick,
),
],
),
],
],
);
}
Widget timePickerField({
required BuildContext context,
required String? text,
required Function(TimeOfDay) onPick,
}) {
return Expanded(
child: TextField(
readOnly: true,
controller: TextEditingController(
text: text,
),
textAlign: TextAlign.center,
onTap: () async {
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime != null) {
onPick.call(pickedTime);
}
},
decoration: const InputDecoration(
hintText: 'Hora',
border: UnderlineInputBorder(),
),
),
);
}
}
+25
View File
@@ -0,0 +1,25 @@
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:flutter/material.dart';
class SearchText extends StatelessWidget {
const SearchText({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 40,
decoration: buildBoxDecoration(),
child: TextField(
decoration: CustomInputs.searchInputDecoration(
hint: 'Buscar', icon: Icons.search_outlined),
),
);
}
BoxDecoration buildBoxDecoration() {
return BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.withOpacity(0.1),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class StatusItem extends StatelessWidget {
final String text;
final Color color;
const StatusItem({
super.key,
required this.text,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: const TextStyle(
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
);
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class TextSeparator extends StatelessWidget {
final String text;
const TextSeparator({super.key, required this.text});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20),
margin: EdgeInsets.only(bottom: 5),
child: Text(
text,
style: GoogleFonts.roboto(
color: Colors.white.withOpacity(0.3), fontSize: 12),
),
);
}
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:flutter/material.dart';
class BlankView extends StatelessWidget {
const BlankView({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
Text('Blank view', style: CustomLabels.h1),
SizedBox(height: 10),
WhiteCard(
title: 'Blank',
child: Text('Blank View'),
),
],
),
);
}
}
+383
View File
@@ -0,0 +1,383 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/calendar_services_provider.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarView extends StatefulWidget {
final String professionalId;
const CalendarView({super.key, required this.professionalId});
@override
State<CalendarView> createState() => _CalendarViewState();
}
class _CalendarViewState extends State<CalendarView> {
List<Service>? _services;
DateTime today = DateTime.now();
late int numDay;
@override
void initState() {
super.initState();
today = DateTime.utc(today.year, today.month, today.day);
numDay = today.weekday;
_fetchProfessionalAndServices();
}
void _fetchProfessionalAndServices() async {
final professionalFormProvider =
Provider.of<ProfessionalFormProvider>(context, listen: false);
final servicesProvider =
Provider.of<CalendarServicesProvider>(context, listen: false);
final proProvider =
Provider.of<ProfessionalProvider>(context, listen: false);
final professional =
await proProvider.getProfessional(widget.professionalId);
professionalFormProvider.setProfesional(professional);
final services =
await servicesProvider.getServicesForProfessional(professional.id);
setState(() {
_services = services;
});
}
void _onDaySelected(DateTime day, DateTime focusedDay) {
setState(() {
today = day;
numDay = today.weekday;
});
_fetchProfessionalAndServices();
}
@override
Widget build(BuildContext context) {
return Consumer<ProfessionalFormProvider>(
builder: (context, professionalFormProvider, child) {
if (professionalFormProvider.profesional == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final profesional = professionalFormProvider.profesional!;
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
title: 'Calendario',
child: Column(
children: [
TableCalendar(
locale: 'es_CO',
firstDay: DateTime.now(),
lastDay: DateTime.now().add(const Duration(days: 180)),
focusedDay: today,
availableGestures: AvailableGestures.all,
onDaySelected: _onDaySelected,
selectedDayPredicate: (day) => isSameDay(day, today),
),
const Divider(height: 0),
SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
),
child: Text(
DateFormat('dd MMMM yyyy', 'es').format(today),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const Divider(height: 0),
Column(
children: [
..._rangesItems(
_getScheduleFromNumDay(numDay, profesional),
context),
const SizedBox(height: 8),
],
),
],
),
),
),
),
],
);
});
}
ScheduleEntity? _getScheduleFromNumDay(
int numDay, Profesional userProfessional) {
switch (numDay) {
case 1:
return userProfessional.schedules.monday;
case 2:
return userProfessional.schedules.tuesday;
case 3:
return userProfessional.schedules.wednesday;
case 4:
return userProfessional.schedules.thursday;
case 5:
return userProfessional.schedules.friday;
case 6:
return userProfessional.schedules.saturday;
case 7:
return userProfessional.schedules.sunday;
default:
return null;
}
}
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context) {
if (schedule == null) {
return [
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text("No hay horarios disponibles"),
)
];
}
if (!schedule.enabled) {
return [
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text("No hay horarios disponibles"),
)
];
}
if (schedule.continuousDay) {
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) { return [
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text("No hay horarios disponibles"),
)
];
}
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range2Hour2!,
);
return rangesItemList(ranges, _services, today, context);
} else {
if (schedule.range1Hour1 == null || schedule.range1Hour2 == null || schedule.range2Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 || schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
return [
const Padding(
padding: EdgeInsets.only(top: 20),
child: Text("No hay horarios disponibles"),
)
];
}
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range1Hour2!,
);
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
schedule.range2Hour1!,
schedule.range2Hour2!,
);
return [
...rangesItemList(ranges1, _services, today, context),
...rangesItemList(ranges2, _services, today, context),
];
}
}
bool _isHora1Ocupada(
TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
if (events != null) {
for (Service event in events) {
if (selectedDay.toIso8601String().split('T').first == event.day) {
if (hora1 == event.range1Hour1) {
return true;
}
}
}
}
return false;
}
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? events,DateTime selectedDay, BuildContext context) {
final currentDateTime = DateTime.now();
return ranges.map((time) {
final selectedDateTime = DateTime(
selectedDay.year,
selectedDay.month,
selectedDay.day,
time.hour,
time.minute,
);
if (selectedDateTime
.isBefore(currentDateTime.add(const Duration(hours: 3)))) {
return Card(
elevation: 4,
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Colors.grey,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
ScheduleEntity.getFormatTime(time) ?? '',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
subtitle: const Text(
'No disponible',
style: TextStyle(
color: Colors.red,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
);
}
if (_isHora1Ocupada(time, events, selectedDay)) {
return Card(
elevation: 4,
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
if (events != null) {
for (Service event in events) {
if (selectedDay.toIso8601String().split('T').first ==
event.day) {
if (time == event.range1Hour1) {
NotificationsService.showSnackbar('Ocupado');
}
}
}
}
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.yellow, Colors.red, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
ScheduleEntity.getFormatTime(time) ?? '',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Ocupado',
style: TextStyle(
color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold),
),
),
);
} else {
return Card(
elevation: 4,
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
Navigator.pop(context, [selectedDay, time]);
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
ScheduleEntity.getFormatTime(time) ?? '',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Disponible',
style: TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
);
}
}).toList();
}
}
+350
View File
@@ -0,0 +1,350 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/message_entity.dart';
import 'package:prosapp_web_app/providers/chat_provider.dart';
import 'package:prosapp_web_app/providers/score_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:provider/provider.dart';
class ChatView extends StatelessWidget {
final String type;
final String serviceId;
final String professionalId;
const ChatView({
super.key,
required this.type,
required this.serviceId,
required this.professionalId,
});
@override
Widget build(BuildContext context) {
final chatProvider = Provider.of<ChatProvider>(context);
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
final scoreProvider = Provider.of<ScoreProvider>(context, listen: false);
scoreProvider.loadReputation(professionalId);
return StreamBuilder(
stream: chatProvider.getChat(serviceId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final chat = snapshot.data;
if (chat == null) {
return const Center(
child: Text('Chat no encontrado'),
);
}
final service = servicesProvider.service?.service;
final user = servicesProvider.service?.user;
final reputation = scoreProvider.reputation;
if (service == null || user == null) {
return const Center(child: CircularProgressIndicator());
}
final image = (user.picture == '' || user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: user.picture!,
);
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 15),
color: const Color(0xFFD6F4FF),
alignment: Alignment.topCenter,
child: Row(
children: [
const SizedBox(width: 20),
SizedBox(
width: 85,
height: 85,
child: ClipOval(child: image),
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
RatingBar.builder(
initialRating: type == 'user'
? calculoRating(reputation.averagePro)
: calculoRating(reputation.average),
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
if (type == 'user')
Text(
'${reputation.averagePro.toStringAsFixed(1)} (${reputation.totalPro.toString()})',
),
if (type == 'professional')
Text(
'${reputation.average.toStringAsFixed(1)} (${reputation.total.toString()})',
),
],
)
],
)
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(top: 10),
reverse: true,
child: Column(
children: _messagesList(chat.messages),
),
),
),
_MessageInput(
serviceId: serviceId,
userId: FirebaseAuth.instance.currentUser!.uid),
],
),
),
);
},
);
}
double calculoRating(double average) {
String numeroString = average.toString();
List<String> partes = numeroString.split('.');
int parteEntera = int.parse(partes[0]);
int parteFraccionaria = partes.length > 1 ? int.parse(partes[1]) : 0;
if (parteFraccionaria >= 3) {
parteFraccionaria = 5;
} else {
parteFraccionaria = 0;
}
// Unir la parte entera y fraccionaria y convertirlo nuevamente a double
double resultado = double.parse('$parteEntera.$parteFraccionaria');
return resultado;
}
List<Widget> _messagesList(List<MessageEntity> messages) {
return messages
.map(
(e) => e.ownerId != FirebaseAuth.instance.currentUser!.uid
? ListTile(
title: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(right: 60),
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Text(
e.content,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
_getFechaHoraFormateada(e.createdAt),
style:
const TextStyle(color: Colors.grey, fontSize: 11),
),
),
],
),
)
: ListTile(
title: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.only(left: 60),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: const BoxDecoration(
color: Color(0xFFD5EFFF),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Text(
e.content,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
_getFechaHoraFormateada(e.createdAt),
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
),
),
),
],
),
),
)
.toList();
}
String _getFechaHoraFormateada(DateTime fecha) {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
final formatter = DateFormat('h:mm a');
if (fecha.day == now.day &&
fecha.month == now.month &&
fecha.year == now.year) {
return 'Hoy - ${formatter.format(fecha)}';
} else if (fecha.day == yesterday.day &&
fecha.month == yesterday.month &&
fecha.year == yesterday.year) {
return 'Ayer - ${formatter.format(fecha)}';
} else {
return '${DateFormat('dd/MM/yyyy').format(fecha)} - ${formatter.format(fecha)}';
}
}
void navigateTo(String routeName) {
NavigationService.replaceTo(routeName);
SideMenuProvider.closeMenu();
}
}
class _MessageInput extends StatefulWidget {
final String serviceId;
final String userId;
const _MessageInput({
required this.serviceId,
required this.userId,
});
@override
State<_MessageInput> createState() => _MessageInputState();
}
class _MessageInputState extends State<_MessageInput> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final chatProvider = Provider.of<ChatProvider>(context, listen: false);
void sendMessage() {
if (_controller.text.trim().isNotEmpty) {
final message = MessageEntity(
ownerId: widget.userId,
content: _controller.text.trim(),
createdAt: DateTime.now(),
);
chatProvider.sendMessage(widget.serviceId, message);
_controller.clear();
}
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Mensaje',
hintStyle: TextStyle(color: Colors.grey[600], fontSize: 16),
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.0),
borderRadius: BorderRadius.circular(50)),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.0),
borderRadius: BorderRadius.circular(50)),
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
filled: true,
fillColor: Colors.grey[200],
),
onSubmitted: (value) => sendMessage(),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () => sendMessage(),
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(30),
),
child: const Center(
child: Icon(
Icons.send,
color: Colors.white,
),
),
),
),
],
),
);
}
}
+398
View File
@@ -0,0 +1,398 @@
import 'dart:async';
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_location_preferences.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/models/usuario_profesional.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/cities_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/utils/local_notifications.dart';
import 'package:prosapp_web_app/utils/network_utility.dart';
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
import 'package:provider/provider.dart';
class DashboardView extends StatefulWidget {
const DashboardView({super.key});
@override
State<DashboardView> createState() => _DashboardViewState();
}
class _DashboardViewState extends State<DashboardView> {
Usuario? user;
List<dynamic> _placesList = [];
final TextEditingController _addressController = TextEditingController();
Timer? _debounce;
UsuarioProfesional? selectedProfessional;
String? selectedProfessionalName;
DateTime? selectedDay;
TimeOfDay? selectedHour;
@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
setState(() {
user = authProvider.user;
});
}
void placeAutoComplete(String query, String _coords) async {
Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", {
"input": query,
"location": _coords,
});
String? response = await NetworkUtility.fetchUrl(uri);
if (response != null) {
if (mounted) {
setState(() {
_placesList = jsonDecode(response.toString())['results'];
});
}
}
}
Future<void> _selectProfessional(BuildContext context) async {
try {
List<dynamic> result = await NavigationService.navigateToFuture(
Flurorouter.professionalsRoute);
final UsuarioProfesional selectedProfessional = result[0];
print('selectedProfessional: $selectedProfessional');
setState(() {
selectedDay = result[1];
selectedHour = result[2];
selectedProfessionalName = selectedProfessional.user.name;
this.selectedProfessional = selectedProfessional;
_addressController.text = selectedProfessional.professionalInfo.address;
});
} catch (e) {
print('debugeando $e');
}
}
@override
void dispose() {
_debounce?.cancel();
_addressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (user == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
bool isUserComplete() {
return user!.name != '' &&
user!.email != '' &&
user!.phone != '' &&
user!.city != '';
}
final citiesProvider = Provider.of<CitiesProvider>(context);
if (citiesProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final _coords = citiesProvider.getCoordsOfCity(user!.city!);
_createService() async {
print('debug ${selectedProfessional?.user.id ?? 'user.id'}');
print('debug ${user!.id ?? 'user.id'}');
print('debug ${_addressController.text ?? 'user.id'}');
try {
double latitude = 0.0;
double longitude = 0.0;
print('debug 1');
Service service = Service(
id: null,
professionalId: selectedProfessional!.user.id,
professionalScored: false,
userId: user!.id,
userScored: false,
address: _addressController.text,
aditionalAddress: '',
latitude: latitude,
longitude: longitude,
day: selectedDay.toString(),
createdAt: Timestamp.now(),
description: '',
range1Hour1: selectedHour!,
range1Hour2: selectedHour!.add(hour: 2),
rate: '',
status: ServiceStatus.pending,
location: ServiceLocationPreferences.delivery,
);
print('debug 1');
await FirebaseFirestore.instance
.collection('services')
.add(service.toDocument());
print('debug 2');
NotificationsService.showSnackbar('Servicio solicitado exitosamente');
if (selectedProfessional != null) {
if (selectedProfessional!.user.token != null) {
LocalNotifications.sendPushNotification(
selectedProfessional!.user.token!,
'Nuevo servicio',
'Tienes una nueva solicitud de servicio pendiente',
);
}
}
_addressController.clear();
setState(() {
selectedProfessional = null;
selectedProfessionalName = null;
selectedDay = null;
selectedHour = null;
});
} catch (e) {
NotificationsService.showSnackBarError(
'$e Error al solicitar el servicio, intenta de nuevo');
}
}
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Stack(
children: [
ListView(
physics: const ClampingScrollPhysics(),
children: [
WhiteCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
isUserComplete()
? TextFormField(
controller: _addressController,
onChanged: (value) {
if (_debounce?.isActive ?? false)
_debounce?.cancel();
_debounce = Timer(
const Duration(milliseconds: 500), () {
String modifiedValue =
value.replaceAll(' ', '_');
placeAutoComplete(modifiedValue, _coords);
});
},
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tu dirección',
label: 'Dirección',
icon: Icons.location_on,
),
)
: GestureDetector(
onTap: () => NotificationsService.showSnackBarError(
'Completa tu perfil para solicitar un servicio'),
child: AbsorbPointer(
child: TextFormField(
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tu dirección',
label: 'Dirección',
icon: Icons.location_on,
),
),
),
),
const SizedBox(height: 10),
GestureDetector(
onTap: () => isUserComplete()
? _selectProfessional(context)
: NotificationsService.showSnackBarError(
'Completa tu perfil para solicitar un servicio'),
child: AbsorbPointer(
child: TextFormField(
controller: TextEditingController(
text: selectedProfessionalName,
),
decoration: CustomInputs.formInputDecoration(
hint: 'Selecciona un Profesional',
label: 'Profesional',
icon: Icons.person_rounded,
),
),
),
),
const SizedBox(height: 10),
if (selectedDay != null && selectedHour != null) ...[
TextFormField(
readOnly: true,
controller: TextEditingController(
text: selectedDay == null
? ''
: DateFormat('dd/MM/yyyy').format(selectedDay!),
),
decoration: CustomInputs.formInputDecoration(
hint: 'Fecha',
label: 'Fecha',
icon: Icons.calendar_month,
),
),
const SizedBox(height: 10),
TextFormField(
readOnly: true,
controller: TextEditingController(
text: selectedHour == null
? ''
: ScheduleEntity.getFormatTime(selectedHour),
),
decoration: CustomInputs.formInputDecoration(
hint: 'Hora',
label: 'Hora',
icon: Icons.watch_later_outlined,
),
),
const SizedBox(height: 10),
],
const SizedBox(height: 10),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 230),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
if (!isUserComplete()) {
NotificationsService.showSnackBarError(
'Completa tu perfil para solicitar un servicio');
return;
}
_createService();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(
Colors.transparent),
),
child: const Text(
'Solicitar cita',
style: TextStyle(color: Colors.white),
),
),
if (selectedDay != null &&
selectedHour != null &&
selectedProfessional != null) ...[
const SizedBox(width: 10),
ElevatedButton(
onPressed: () async {
_addressController.clear();
selectedProfessional = null;
selectedDay = null;
selectedHour = null;
setState(() {});
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.red,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5)))),
shadowColor: WidgetStateProperty.all(
Colors.transparent)),
child: const Icon(
Icons.close_rounded,
color: Colors.white,
),
),
],
],
),
),
),
const SizedBox(height: 8),
],
),
),
],
),
Positioned(
left: 20,
right: 20,
top: 80,
child: _placesList.isNotEmpty
? Material(
elevation: 5.0,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: ListView.builder(
shrinkWrap: true,
itemCount: _placesList.length,
itemBuilder: (context, index) {
return ListTile(
title:
Text(_placesList[index]['formatted_address']),
dense: true,
visualDensity: VisualDensity.compact,
onTap: () {
setState(() {
_addressController.text =
_placesList[index]['formatted_address'];
_placesList = [];
});
},
);
},
),
),
)
: Container(),
),
],
),
),
);
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/email_form_provider.dart';
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:provider/provider.dart';
class EmailView extends StatelessWidget {
const EmailView({super.key});
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController =
TextEditingController();
final RegExp emailRegex =
RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
RegExp passwordRegex = RegExp(r'^(?=.*?[0-9])');
return ChangeNotifierProvider(
create: (_) => EmailFormProvider(),
child: Builder(builder: (context) {
final emailFormProvider =
Provider.of<EmailFormProvider>(context, listen: false);
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 370),
child: Form(
autovalidateMode:
AutovalidateMode.onUserInteraction,
key: emailFormProvider.formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Ingresa un email';
}
if (!emailRegex.hasMatch(value)) {
return 'Ingresa un email válido';
}
return null;
},
onChanged: (email) =>
emailFormProvider.email = email,
decoration:
CustomInputs.loginInputDecoration(
hint: 'Ingresa tu email',
label: 'Email',
icon: Icons.email_outlined,
),
),
const SizedBox(height: 20),
TextFormField(
controller: _passwordController,
obscureText: true,
onChanged: (value) =>
emailFormProvider.password = value,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Ingresa una contraseña';
}
if (value.length < 6) {
return 'La contraseña debe tener al menos 6 caracteres';
}
if (!passwordRegex.hasMatch(value)) {
return 'La contraseña debe tener al menos un número';
}
return null;
},
decoration:
CustomInputs.loginInputDecoration(
hint: 'Ingresa tu contraseña',
label: 'Contraseña',
icon: Icons.lock_outline,
),
),
const SizedBox(height: 20),
TextFormField(
obscureText: true,
controller: _confirmPasswordController,
onChanged: (value) =>
emailFormProvider.password = value,
validator: (value) {
if (value != _passwordController.text) {
return 'Las contraseñas no coinciden';
}
if (value!.isEmpty) {
return 'La contraseña es obligatoria';
}
return null;
},
decoration:
CustomInputs.loginInputDecoration(
hint: 'Confirma tu contraseña',
label: 'Confirmar contraseña',
icon: Icons.lock_outline,
),
),
const SizedBox(height: 20),
CustomOutlinedButton(
onPressed: () async {
final isValid =
emailFormProvider.validateForm();
if (isValid) {
await authProvider.addEmailAndPassword(
emailFormProvider.email,
emailFormProvider.password);
}
},
text: "Guardar",
color: Colors.blue,
),
const SizedBox(height: 20),
],
),
),
),
),
),
],
),
),
),
),
],
);
}),
);
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/login_form_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:email_validator/email_validator.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
return ChangeNotifierProvider(
create: (_) => LoginFormProvider(),
child: Builder(builder: (context) {
final loginFormProvider =
Provider.of<LoginFormProvider>(context, listen: false);
return Container(
margin: const EdgeInsets.only(top: 100),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 370),
child: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
key: loginFormProvider.formKey,
child: Column(
children: [
// Email
TextFormField(
onFieldSubmitted: (_) =>
onFormSubmit(loginFormProvider, authProvider),
validator: (value) {
if (!EmailValidator.validate(value ?? '')) {
return 'Email no válido';
}
return null;
},
onChanged: (value) => loginFormProvider.email = value,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingresa tu correo',
label: 'Email',
icon: Icons.email_outlined,
),
),
const SizedBox(height: 20),
// Password
TextFormField(
onFieldSubmitted: (_) =>
onFormSubmit(loginFormProvider, authProvider),
validator: (value) {
if (value == null || value.isEmpty) {
return "Ingresa tu contraseña";
}
if (value.length < 8) {
return "La contraseña debe tener al menos 8 caracteres";
}
return null; // Válido
},
onChanged: (value) => loginFormProvider.password = value,
obscureText: true,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingresa tu contraseña',
label: 'Contraseña',
icon: Icons.lock_outline,
),
),
const SizedBox(height: 20),
CustomOutlinedButton(
onPressed: () =>
onFormSubmit(loginFormProvider, authProvider),
text: "Ingresar",
color: Colors.blue,
),
const SizedBox(height: 20),
LinkText(
text: "Nueva cuenta",
onPressed: () {
Navigator.pushReplacementNamed(
context, Flurorouter.registerRoute);
},
),
const SizedBox(height: 5),
LinkText(
text: "Entrar con celular",
onPressed: () {
Navigator.pushReplacementNamed(
context, Flurorouter.phoneLoginRoute);
},
),
],
),
),
),
),
);
}),
);
}
void onFormSubmit(
LoginFormProvider loginFormProvider, AuthProvider authProvider) async {
final isValid = loginFormProvider.validateForm();
if (isValid) {
await authProvider.login(
loginFormProvider.email,
loginFormProvider.password,
);
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class NoPageFoundView extends StatelessWidget {
const NoPageFoundView({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'404 - No Page Found',
style: GoogleFonts.montserratAlternates(
fontSize: 50, fontWeight: FontWeight.bold),
),
);
}
}
+128
View File
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:provider/provider.dart';
class PhoneLoginView extends StatelessWidget {
const PhoneLoginView({super.key});
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final TextEditingController _phoneController = TextEditingController();
return ChangeNotifierProvider(
create: (_) => PhoneFormProvider(),
child: Builder(builder: (context) {
final phoneFormProvider =
Provider.of<PhoneFormProvider>(context, listen: false);
return Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 370),
child: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
key: phoneFormProvider.formKey,
child: Column(
children: [
TextFormField(
controller: _phoneController,
validator: (value) {
if (value == null ||
value.isEmpty ||
value.length < 10) {
return 'Ingresa un número de teléfono válido';
}
return null;
},
onChanged: (value) {
phoneFormProvider.phone = '+57${value.trim()}';
},
keyboardType: TextInputType.phone,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingresa tu número de teléfono',
label: 'Número de Teléfono',
icon: Icons.phone_android_outlined,
),
),
const SizedBox(height: 20),
CustomOutlinedButton(
onPressed: () async {
final isValid = phoneFormProvider.validateForm();
if (isValid) {
await authProvider
.verifyPhoneNumber(phoneFormProvider.phone);
showDialog(
context: context,
builder: (context) =>
_buildOtpModal(context, authProvider),
);
}
},
text: "Enviar código",
color: Colors.blue,
),
const SizedBox(height: 20),
LinkText(
text: "Entrar con correo",
onPressed: () {
Navigator.pushReplacementNamed(
context, Flurorouter.loginRoute);
},
),
],
),
),
),
),
);
}),
);
}
Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) {
final _otpController = TextEditingController();
return AlertDialog(
title: const Text('Ingresar código OTP'),
content: TextField(
controller: _otpController,
decoration: const InputDecoration(labelText: 'Código OTP'),
keyboardType: TextInputType.number,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () async {
final otp = _otpController.text.trim();
if (otp.isNotEmpty) {
await authProvider.signInWithOTP(otp);
Navigator.of(context).pop();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Por favor ingresa el código OTP')),
);
}
},
child: const Text('Verificar OTP'),
),
],
);
}
}
+141
View File
@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:provider/provider.dart';
class PhoneView extends StatelessWidget {
const PhoneView({super.key});
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final TextEditingController _phoneController = TextEditingController();
return ChangeNotifierProvider(
create: (_) => PhoneFormProvider(),
child: Builder(builder: (context) {
final phoneFormProvider =
Provider.of<PhoneFormProvider>(context, listen: false);
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 370),
child: Form(
autovalidateMode:
AutovalidateMode.onUserInteraction,
key: phoneFormProvider.formKey,
child: Column(
children: [
TextFormField(
controller: _phoneController,
validator: (value) {
if (value == null ||
value.isEmpty ||
value.length < 10) {
return 'Ingresa un número de teléfono válido';
}
return null;
},
onChanged: (value) {
phoneFormProvider.phone =
'+57${value.trim()}';
},
keyboardType: TextInputType.phone,
decoration:
CustomInputs.loginInputDecoration(
hint: 'Ingresa tu número de teléfono',
label: 'Número de Teléfono',
icon: Icons.phone_android_outlined,
),
),
const SizedBox(height: 20),
CustomOutlinedButton(
onPressed: () async {
final isValid =
phoneFormProvider.validateForm();
if (isValid) {
await authProvider
.verifyPhoneNumberForLink(
phoneFormProvider.phone);
showDialog(
context: context,
builder: (context) => _buildOtpModal(
context, authProvider),
);
}
},
text: "Enviar código",
color: Colors.blue,
),
const SizedBox(height: 20),
],
),
),
),
),
),
],
),
),
),
),
],
);
}),
);
}
Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) {
final _otpController = TextEditingController();
return AlertDialog(
title: const Text('Ingresar código OTP'),
content: TextField(
controller: _otpController,
decoration: const InputDecoration(labelText: 'Código OTP'),
keyboardType: TextInputType.number,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () async {
final otp = _otpController.text.trim();
if (otp.isNotEmpty) {
await authProvider.linkPhoneWithOTP(otp);
Navigator.of(context).pop();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Por favor ingresa el código OTP')),
);
}
},
child: const Text('Verificar OTP'),
),
],
);
}
}
@@ -0,0 +1,356 @@
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:flutter/material.dart';
class ProfessionalCalendarView extends StatefulWidget {
const ProfessionalCalendarView({super.key});
@override
State<ProfessionalCalendarView> createState() =>
_ProfessionalCalendarViewState();
}
class _ProfessionalCalendarViewState extends State<ProfessionalCalendarView> {
DateTime today = DateTime.now();
late int numDay;
List<Service>? _services;
Usuario? user;
@override
void initState() {
super.initState();
numDay = today.weekday;
_fetchProfessionalAndServices();
}
void _fetchProfessionalAndServices() async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final professionalFormProvider = Provider.of<ProfessionalFormProvider>(context, listen: false);
final servicesProvider = Provider.of<ServicesProvider>(context, listen: false);
final proProvider = Provider.of<ProfessionalProvider>(context, listen: false);
final professional = await proProvider.getProfessional(authProvider.user!.id);
professionalFormProvider.setProfesional(professional);
final services = await servicesProvider.getServicesForProfessional(professional.id);
setState(() {
_services = services;
user = authProvider.user;
});
}
void _onDaySelected(DateTime day, DateTime focusedDay) {
setState(() {
today = day;
numDay = today.weekday;
});
_fetchProfessionalAndServices();
}
@override
Widget build(BuildContext context) {
return Consumer<ProfessionalFormProvider>(
builder: (context, professionalFormProvider, child) {
if (professionalFormProvider.profesional == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final profesional = professionalFormProvider.profesional!;
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
title: 'Calendario',
child: Column(
children: [
TableCalendar(
locale: 'es_CO',
firstDay: DateTime.now(),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: today,
availableGestures: AvailableGestures.all,
onDaySelected: _onDaySelected,
selectedDayPredicate: (day) => isSameDay(day, today),
),
const Divider(height: 0),
SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
),
child: Text(
DateFormat('dd MMMM yyyy', 'es').format(today),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const Divider(height: 0),
Column(
children: [
..._rangesItems(_getScheduleFromNumDay(numDay, profesional),context),
],
),
],
),
),
),
),
],
);
},
);
}
ScheduleEntity? _getScheduleFromNumDay(int numDay, Profesional userProfessional) {
switch (numDay) {
case 1:
return userProfessional.schedules.monday;
case 2:
return userProfessional.schedules.tuesday;
case 3:
return userProfessional.schedules.wednesday;
case 4:
return userProfessional.schedules.thursday;
case 5:
return userProfessional.schedules.friday;
case 6:
return userProfessional.schedules.saturday;
case 7:
return userProfessional.schedules.sunday;
default:
return null;
}
}
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context) {
if (schedule == null ||
!schedule.enabled ||
schedule.range1Hour1 == null ||
schedule.range2Hour2 == null) {
return [
const Padding(
padding: EdgeInsets.symmetric(vertical: 25),
child: Text("No hay horarios disponibles"),
)
];
}
if (schedule.continuousDay) {
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range2Hour2!,
);
return rangesItemList(ranges, _services, today, context);
}
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range1Hour2!,
);
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
schedule.range2Hour1!,
schedule.range2Hour2!,
);
return [
...rangesItemList(ranges1, _services, today, context),
...rangesItemList(ranges2, _services, today, context),
];
}
bool _isHora1Ocupada(
TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
if (events != null) {
for (Service event in events) {
if (selectedDay.toIso8601String().split('T').first == event.day) {
if (hora1 == event.range1Hour1) {
return true;
}
}
}
}
return false;
}
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? events,
DateTime selectedDay, BuildContext context) {
return ranges.map((time) {
if (_isHora1Ocupada(time, events, selectedDay)) {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
if (events != null) {
for (Service event in events) {
if (selectedDay.toIso8601String().split('T').first ==
event.day) {
if (time == event.range1Hour1) {
if (event.userId == event.professionalId) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Horario ocupado por ti'),
),
);
} else {
// Navigator.push(
// context,
// CupertinoPageRoute(
// builder: (context) => ProfessionalServiceScreen(
// serviceId: event.id!,
// ),
// ),
// );
}
}
}
}
}
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.yellow, Colors.red, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
ScheduleEntity.getFormatTime(time) ?? '',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Ocupado',
style: TextStyle(
color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold),
),
),
);
} else {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Reservar hora'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'¿Estás seguro de que deseas reservar a las ${ScheduleEntity.getFormatTime(time)} del ${DateFormat('dd-MM-yyyy').format(today)}?',
),
const SizedBox(height: 10),
const Text(
'⚠️ Esta acción no se puede deshacer ⚠️',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
},
child: const Text('No, cancelar'),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: const Text('Sí, reservar'),
),
],
);
},
);
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
ScheduleEntity.getFormatTime(time) ?? '',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Disponible',
style: TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
);
}
}).toList();
}
}
+535
View File
@@ -0,0 +1,535 @@
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:prosapp_web_app/models/location_preferences.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
import 'package:prosapp_web_app/providers/settings_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProfessionalProfileView extends StatefulWidget {
const ProfessionalProfileView({super.key});
@override
State<ProfessionalProfileView> createState() =>
_ProfessionalProfileViewState();
}
class _ProfessionalProfileViewState extends State<ProfessionalProfileView> {
Usuario? user;
late ProfessionalFormProvider professionalFormProvider;
@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final profileFormProvider =
Provider.of<ProfileFormProvider>(context, listen: false);
professionalFormProvider =
Provider.of<ProfessionalFormProvider>(context, listen: false);
final proProvider =
Provider.of<ProfessionalProvider>(context, listen: false);
proProvider.getProfessional(authProvider.user!.id).then((value) {
professionalFormProvider.setProfesional(value);
});
profileFormProvider.user = authProvider.user;
setState(() {
user = authProvider.user;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth < 700) {
return ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewBody()],
);
} else {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewBody()],
),
);
}
});
}
}
class _ProfileViewBody extends StatelessWidget {
const _ProfileViewBody();
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: const Column(
children: [
_AvatarContainer(containerFull: true),
_ProfileViewForm(),
],
),
),
);
}
}
class _ProfileViewForm extends StatefulWidget {
const _ProfileViewForm();
@override
State<_ProfileViewForm> createState() => _ProfileViewFormState();
}
class _ProfileViewFormState extends State<_ProfileViewForm> {
bool _serviceDeliverySwitchValue = false;
bool _serviceSiteSwitchValue = false;
bool _serviceRateSwitchValue = false;
bool first = true;
@override
Widget build(BuildContext context) {
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
final settingsProvider = Provider.of<SettingsProvider>(context);
final user = profileFormProvider.user!;
return Consumer<ProfessionalFormProvider>(
builder: (context, professionalFormProvider, child) {
if (professionalFormProvider.profesional == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (settingsProvider.settings == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final settings = settingsProvider.settings!;
final profesional = professionalFormProvider.profesional!;
if (first) {
first = false;
if (settings.tarifas) {
_serviceRateSwitchValue = profesional.ratePreferences;
} else {
_serviceRateSwitchValue = false;
}
if (profesional.locationPreferences == LocationPreferences.both) {
_serviceSiteSwitchValue = true;
_serviceDeliverySwitchValue = true;
} else if (profesional.locationPreferences ==
LocationPreferences.office) {
_serviceSiteSwitchValue = true;
_serviceDeliverySwitchValue = false;
} else if (profesional.locationPreferences ==
LocationPreferences.delivery) {
_serviceSiteSwitchValue = false;
_serviceDeliverySwitchValue = true;
}
}
return LayoutBuilder(builder: (context, constraints) {
double columnWidth = constraints.maxWidth < 600 ? 95 : 150;
return WhiteCard(
title: 'Información profesional',
child: Form(
key: professionalFormProvider.profileFormKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
const SizedBox(height: 10),
SwitchListTile(
title: const Text('Servicio a domicilio'),
value: _serviceDeliverySwitchValue,
onChanged: (value) {
setState(() {
_serviceDeliverySwitchValue = value;
if (settings.domicilios == true) {
_serviceDeliverySwitchValue = value;
if (!_serviceDeliverySwitchValue) {
_serviceSiteSwitchValue = true;
}
} else {
_serviceDeliverySwitchValue = false;
}
});
},
),
const SizedBox(height: 10),
SwitchListTile(
title: const Text('Servicio en sitio / consultorio'),
value: _serviceSiteSwitchValue,
onChanged: (value) {
setState(() {
if (settings.domicilios == true) {
_serviceSiteSwitchValue = value;
if (!_serviceSiteSwitchValue) {
_serviceDeliverySwitchValue = true;
}
} else {
_serviceSiteSwitchValue = true;
_serviceDeliverySwitchValue = false;
}
});
},
),
if (_serviceSiteSwitchValue)
Column(
children: [
TextFormField(
initialValue: profesional.address,
// validator: (value) {
// if (value == null || value.isEmpty) {
// return 'La dirección es obligatoria';
// }
// return null;
// },
onChanged: (value) {},
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tu dirección',
label: 'Dirección',
icon: Icons.location_on_outlined,
),
),
const SizedBox(height: 12),
TextFormField(
initialValue: profesional.aditionalAddress,
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tu piso / apartamento / conjunto',
label: 'Piso / Apartamento / Conjunto',
icon: Icons.email_outlined,
),
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
aditionalAddress: value);
},
),
],
),
if (settings.tarifas == true) ...[
const SizedBox(height: 20),
SwitchListTile(
title: const Text('Tarifa'),
value: _serviceRateSwitchValue,
onChanged: (value) {
setState(() {
_serviceRateSwitchValue = value;
});
},
),
if (_serviceRateSwitchValue)
Column(
children: [
TextFormField(
keyboardType: TextInputType.number,
initialValue: profesional.rate,
validator: (value) {
if (value == null || value.isEmpty) {
return 'La tarifa es obligatoria';
}
final numericRegex = RegExp(r'^[0-9]+$');
if (!numericRegex.hasMatch(value)) {
return 'Por favor, ingresa solo números';
}
return null;
},
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
rate: value);
},
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa la tarifa del servicio',
label: 'Tarifa',
icon: Icons.attach_money,
),
),
],
),
],
const SizedBox(height: 20),
Text('Metodos de pago', style: CustomLabels.h3),
CheckboxListTile(
title: const Text('Datafono'),
value: profesional.paymentMethods.datafono,
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
paymentMethods: profesional.paymentMethods
.copyWith(datafono: value));
}),
const SizedBox(height: 10),
CheckboxListTile(
title: const Text('Nequi'),
value: profesional.paymentMethods.nequi,
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
paymentMethods: profesional.paymentMethods
.copyWith(nequi: value));
}),
const SizedBox(height: 10),
CheckboxListTile(
title: const Text('Transferencia bancaria'),
value: profesional.paymentMethods.transferencia,
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
paymentMethods: profesional.paymentMethods
.copyWith(transferencia: value));
}),
const SizedBox(height: 20),
Text('Horarios de atenciòn', style: CustomLabels.h3),
const SizedBox(height: 10),
GestureDetector(
onTap: () {
NavigationService.replaceTo(
Flurorouter.professionalScheduleRoute);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Table(
columnWidths: {
0: FixedColumnWidth(columnWidth),
},
border:
TableBorder.all(color: Colors.grey.withOpacity(0.3)),
children: [
_buildTableRow(
'Lunes', profesional.schedules.monday, context),
_buildTableRow(
'Martes', profesional.schedules.tuesday, context),
_buildTableRow('Miercoles',
profesional.schedules.wednesday, context),
_buildTableRow(
'Jueves', profesional.schedules.thursday, context),
_buildTableRow(
'Viernes', profesional.schedules.friday, context),
_buildTableRow(
'Sabado', profesional.schedules.saturday, context),
_buildTableRow(
'Domingo', profesional.schedules.sunday, context),
],
),
),
),
const SizedBox(height: 20),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () async {
LocationPreferences locationPreferences;
if (_serviceDeliverySwitchValue &&
_serviceSiteSwitchValue) {
locationPreferences = LocationPreferences.both;
} else if (_serviceDeliverySwitchValue) {
locationPreferences = LocationPreferences.delivery;
} else if (_serviceSiteSwitchValue) {
locationPreferences = LocationPreferences.office;
} else {
locationPreferences = LocationPreferences.office;
}
professionalFormProvider.copyProfesionalWith(
locationPreferences: locationPreferences,
ratePreferences: _serviceRateSwitchValue,
);
await professionalFormProvider
.updateProfesionalProfileInfo(user.id);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape:
WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text(
'Guardar',
style: TextStyle(color: Colors.white),
),
),
),
const SizedBox(height: 10),
],
),
),
);
});
});
}
TableRow _buildTableRow(
String day, ScheduleEntity schedules, BuildContext context) {
return TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(
color: Colors.blueAccent,
),
child: Text(
day,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: Text(
timeList(schedules, context),
style: const TextStyle(
color: Colors.black87,
),
),
),
),
],
);
}
String timeList(ScheduleEntity? schedule, BuildContext context) {
if (schedule == null) {
return 'No hay horarios';
}
if (!schedule.enabled) {
return 'No hay horarios';
}
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null) {
return 'No hay horarios';
}
if (schedule.continuousDay) {
return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}';
} else {
if (schedule.range1Hour2 == null || schedule.range2Hour1 == null) {
return 'No hay horarios';
}
return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range1Hour2)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}';
}
}
}
class _AvatarContainer extends StatelessWidget {
final bool containerFull;
const _AvatarContainer({required this.containerFull});
@override
Widget build(BuildContext context) {
final user = Provider.of<AuthProvider>(context).user!;
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
final image = (profileFormProvider.user!.picture == '' ||
profileFormProvider.user!.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: profileFormProvider.user!.picture!,
);
return WhiteCard(
width: containerFull ? null : 250,
child: SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(user.name, style: CustomLabels.h2),
const SizedBox(height: 10),
SizedBox(
width: 160,
height: 160,
child: Stack(
children: [
SizedBox(
width: 200,
height: 200,
child: ClipOval(child: image),
),
Positioned(
bottom: 5,
right: 5,
child: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
border: Border.all(color: Colors.white, width: 5),
),
child: FloatingActionButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
Uint8List? fileBytes = file.bytes;
if (fileBytes != null) {
NotificationsService.showBusyIndicator(context);
await profileFormProvider
.uploadPicture(fileBytes);
Provider.of<AuthProvider>(context, listen: false)
.refreshUser();
Navigator.pop(context);
}
} else {}
},
backgroundColor: Colors.indigo,
elevation: 0,
child: const Icon(
Icons.camera_alt_outlined,
size: 20,
color: Colors.white,
),
),
),
)
],
),
),
const SizedBox(height: 20),
],
),
),
);
}
}
+172
View File
@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/usuario_profesional.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:provider/provider.dart';
import 'package:prosapp_web_app/providers/professionals_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
class ProfessionalsView extends StatelessWidget {
const ProfessionalsView({super.key});
@override
Widget build(BuildContext context) {
final professionalsProvider = Provider.of<ProfessionalsProvider>(context);
return LayoutBuilder(
builder: (context, constraints) {
int crossAxisCount;
double childAspectRatio;
if (constraints.maxWidth >= 1540) {
crossAxisCount = 5;
childAspectRatio = 0.75;
} else if (constraints.maxWidth >= 1000) {
crossAxisCount = 4;
childAspectRatio = 0.65;
} else if (constraints.maxWidth >= 650) {
crossAxisCount = 3;
childAspectRatio = 0.55;
} else if (constraints.maxWidth >= 400) {
crossAxisCount = 2;
childAspectRatio = 0.55;
} else {
crossAxisCount = 1;
childAspectRatio = 0.7;
}
if (professionalsProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: childAspectRatio,
),
itemCount: professionalsProvider.professionals.length,
itemBuilder: (context, index) {
UsuarioProfesional data = professionalsProvider.professionals[index];
final image = (data.user.picture == '' || data.user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: data.user.picture!,
);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () async {
final List<dynamic> result = await NavigationService.navigateToFuture('/dashboard/calendar/${data.user.id}');
DateTime day = result[0];
TimeOfDay time = result[1];
Navigator.pop(context, [data, day, time]);
},
child: WhiteCard(
title: data.user.name.toUpperCase(),
child: Column(
children: [
Row(
children: [
const Icon(
Icons.star,
size: 15,
color: Colors.yellow,
),
const SizedBox(width: 3),
Text(
data.averageScore.toString(),
style: CustomLabels.h4,
),
],
),
Center(
child: SizedBox(
width: 100,
height: 100,
child: ClipOval(
child: image,
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 5),
Row(
children: [
const Icon(Icons.location_on_outlined,
size: 15),
const SizedBox(width: 2),
Text(
data.user.city ?? '',
textAlign: TextAlign.center,
),
],
),
Row(
children: [
const Icon(Icons.work_outline, size: 15),
const SizedBox(width: 5),
Text(data.professionalInfo.profession),
],
),
const Divider(),
if (data.professionalInfo.paymentMethods.datafono ||
data.professionalInfo.paymentMethods.nequi ||
data.professionalInfo.paymentMethods
.transferencia) ...[
const Row(
children: [
Icon(Icons.payment_outlined, size: 15),
SizedBox(width: 5),
Text('Metodos de pago'),
],
),
Visibility(
visible: data
.professionalInfo.paymentMethods.datafono,
child: const Text('- Datafono'),
),
Visibility(
visible:
data.professionalInfo.paymentMethods.nequi,
child: const Text('- Nequi'),
),
Visibility(
visible: data.professionalInfo.paymentMethods
.transferencia,
child: const Text('- Transferencia'),
),
] else
const Text(
'No hay metodos de pago registrados',
style: TextStyle(
fontSize: 14,
color: Colors.black45,
),
),
],
),
),
],
),
),
),
);
},
);
},
);
}
}
+324
View File
@@ -0,0 +1,324 @@
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:prosapp_web_app/models/city.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/cities_provider.dart';
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProfileView extends StatefulWidget {
const ProfileView({super.key});
@override
State<ProfileView> createState() => _ProfileViewState();
}
class _ProfileViewState extends State<ProfileView> {
Usuario? user;
List<City> cities = [];
@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final profileFormProvider =
Provider.of<ProfileFormProvider>(context, listen: false);
final citiesProvider = Provider.of<CitiesProvider>(context, listen: false);
profileFormProvider.user = authProvider.user;
setState(() {
cities = citiesProvider.cities;
user = authProvider.user;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth < 700) {
return ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewBody()],
);
} else {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewBody()],
),
);
}
});
}
}
class _ProfileViewBody extends StatelessWidget {
const _ProfileViewBody();
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth < 700) {
return const Column(
children: [
_AvatarContainer(containerFull: true),
_ProfileViewForm(),
],
);
} else {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Table(
columnWidths: const {
0: FixedColumnWidth(250),
},
children: const [
TableRow(
children: [
_AvatarContainer(containerFull: true),
_ProfileViewForm(),
],
)
],
),
),
);
}
});
}
}
class _ProfileViewForm extends StatelessWidget {
const _ProfileViewForm();
@override
Widget build(BuildContext context) {
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
final citiesProvider = Provider.of<CitiesProvider>(context);
final cities = citiesProvider.cities;
final user = profileFormProvider.user!;
return WhiteCard(
title: 'Información general',
child: Form(
key: profileFormProvider.formKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
const SizedBox(height: 10),
TextFormField(
initialValue: user.name,
validator: (value) {
if (value == null || value.isEmpty) {
return 'El nombre es obligatorio';
}
if (value.trim().length < 4) {
return 'El nombre debe tener al menos 4 caracteres';
}
return null;
},
onChanged: (value) {
profileFormProvider.copyUserWith(name: value);
},
decoration: CustomInputs.formInputDecoration(
hint: 'Nombre de usuario',
label: 'Nombre',
icon: Icons.person_outline,
),
),
const SizedBox(height: 20),
TextFormField(
readOnly: true,
onTap: user.phone == null || user.phone!.isEmpty
? () {
NavigationService.navigateTo(Flurorouter.phoneRoute);
}
: null,
initialValue: user.phone ?? '',
decoration: CustomInputs.formInputDecoration(
hint: 'Número de teléfono',
label: 'Teléfono',
icon: Icons.phone,
),
),
const SizedBox(height: 20),
TextFormField(
readOnly: true,
onTap: user.email == null || user.email!.isEmpty
? () {
NavigationService.navigateTo(Flurorouter.emailRoute);
}
: null,
initialValue: user.email ?? '',
decoration: CustomInputs.formInputDecoration(
hint: 'Correo del usuario',
label: 'Correo',
icon: Icons.email_outlined,
),
),
const SizedBox(height: 20),
DropdownButtonFormField(
validator: (value) {
if (value == null) {
return 'La ciudad es obligatoria';
}
return null;
},
value: user.city == '' ? null : user.city,
decoration: CustomInputs.formInputDecoration(
hint: 'Selecciona tu ciudad',
label: 'Ciudad',
icon: Icons.location_city_outlined,
),
items: cities.map((City ciudad) {
return DropdownMenuItem<String>(
value: ciudad.cityName,
child: Text('${ciudad.cityName} - ${ciudad.stateOfCity}',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
)),
);
}).toList(),
onChanged: (value) {
profileFormProvider.copyUserWith(city: value!);
}),
const SizedBox(height: 20),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () async {
await profileFormProvider.updateUserInfo();
Provider.of<AuthProvider>(context, listen: false)
.refreshUser();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Guardar',
style: TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 10),
],
),
),
);
}
}
class _AvatarContainer extends StatelessWidget {
final bool containerFull;
const _AvatarContainer({required this.containerFull});
@override
Widget build(BuildContext context) {
final user = Provider.of<AuthProvider>(context).user!;
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
final image = (profileFormProvider.user!.picture == '' ||
profileFormProvider.user!.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: profileFormProvider.user!.picture!,
);
return WhiteCard(
width: containerFull ? null : 250,
child: SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
user.name,
style: CustomLabels.h2,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
SizedBox(
width: 160,
height: 160,
child: Stack(
children: [
SizedBox(
width: 200,
height: 200,
child: ClipOval(child: image),
),
Positioned(
bottom: 5,
right: 5,
child: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
border: Border.all(color: Colors.white, width: 5),
),
child: FloatingActionButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
Uint8List? fileBytes = file.bytes;
if (fileBytes != null) {
NotificationsService.showBusyIndicator(context);
await profileFormProvider
.uploadPicture(fileBytes);
Provider.of<AuthProvider>(context, listen: false)
.refreshUser();
Navigator.pop(context);
}
} else {}
},
backgroundColor: Colors.indigo,
elevation: 0,
child: const Icon(
Icons.camera_alt_outlined,
size: 20,
color: Colors.white,
),
),
),
)
],
),
),
const SizedBox(height: 20),
],
),
),
);
}
}
+274
View File
@@ -0,0 +1,274 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/providers/score_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:provider/provider.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
class RatingView extends StatefulWidget {
final String type;
final String serviceId;
final String professionalId;
const RatingView({
super.key,
required this.type,
required this.serviceId,
required this.professionalId,
});
@override
State<RatingView> createState() => _RatingViewState();
}
class _RatingViewState extends State<RatingView> {
double _rating = 1.0;
TextEditingController commentController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
final scoreProvider = Provider.of<ScoreProvider>(context);
if (widget.type == 'user') {
servicesProvider.getServiceForUser(widget.serviceId);
}
if (widget.type == 'professional') {
servicesProvider.getServiceForProfessional(widget.serviceId);
}
return Consumer<ServicesProvider>(
builder: (context, servicesProvider, child) {
if (servicesProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final service = servicesProvider.service!.service;
final user = servicesProvider.service!.user;
if (widget.type == 'user' && service.professionalScored == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
navigateTo(Flurorouter.dashboardRoute);
});
}
if (widget.type == 'professional' && service.userScored == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
navigateTo(Flurorouter.dashboardRoute);
});
}
final image = (user.picture == '' || user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: user.picture!,
);
return ListView(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 100,
height: 100,
child: ClipOval(child: image),
),
const SizedBox(height: 16.0),
Text(
user.name.toUpperCase(),
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
const Text(
'Califica el servicio',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
RatingBar.builder(
initialRating: _rating,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 35,
glow: false,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 5),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {
setState(() {
_rating = rating;
});
},
ignoreGestures: false,
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
customMessage(_rating),
style: const TextStyle(
fontSize: 17,
color: Color(0xFF2BA4EC),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 80, vertical: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comentario',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
TextFormField(
decoration: const InputDecoration(
labelStyle: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600,
),
),
maxLines: null,
maxLength: 500,
keyboardType: TextInputType.multiline,
controller: commentController,
),
],
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: ElevatedButton(
onPressed: () async {
if (widget.type == 'user') {
final newComment = CommentEntity(
content: commentController.text,
score: _rating,
isFromUser: true,
serviceId: widget.serviceId,
createdAt: Timestamp.now(),
authorId: service.userId,
destinationId: service.professionalId,
);
await scoreProvider.addComment(
newComment,
);
servicesProvider.changeProfessionalScored(
widget.serviceId);
}
if (widget.type == 'professional') {
final newComment = CommentEntity(
content: commentController.text,
score: _rating,
isFromUser: false,
serviceId: widget.serviceId,
createdAt: Timestamp.now(),
authorId: service.professionalId,
destinationId: service.userId,
);
await scoreProvider.addComment(
newComment,
);
servicesProvider
.changeUserScored(widget.serviceId);
}
navigateTo(Flurorouter.dashboardRoute);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(
Colors.transparent),
),
child: const Text(
'Enviar calificación',
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
],
),
),
),
),
],
);
});
}
void navigateTo(String routeName) {
NavigationService.replaceTo(routeName);
SideMenuProvider.closeMenu();
}
String customMessage(double rating) {
if (rating > 4.0) {
return '¡Excelente! 👏🌟';
}
if (rating < 2.0) {
return '¡Malo! 😔❌';
}
if (rating <= 4.0 && rating >= 3.0) {
return '¡Bueno! 👍😊';
}
if (rating < 3.0 && rating >= 2.0) {
return '¡Regular! 😐🔄';
}
return '';
}
}
+135
View File
@@ -0,0 +1,135 @@
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/register_form_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:email_validator/email_validator.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RegisterView extends StatelessWidget {
const RegisterView({super.key});
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
return ChangeNotifierProvider(
create: (_) => RegisterFormProvider(),
child: Builder(builder: (context) {
final registerFormProvider =
Provider.of<RegisterFormProvider>(context, listen: false);
return Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 370),
child: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
key: registerFormProvider.formKey,
child: Column(
children: [
// Name
TextFormField(
validator: (value) {
if (value == null ||
value.isEmpty ||
value.trim().isEmpty) {
return 'El nombre es obligatorio';
}
if (value.trim().length < 3) {
return 'El nombre debe tener al menos 3 caracteres';
}
return null;
},
onChanged: (value) => registerFormProvider.name = value,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingrese su nombre',
label: 'Nombre',
icon: Icons.person_outline,
),
),
const SizedBox(height: 20),
// Email
TextFormField(
validator: (value) {
if (!EmailValidator.validate(value ?? '')) {
return 'Email no válido';
}
return null;
},
onChanged: (value) => registerFormProvider.email = value,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingresa tu correo',
label: 'Email',
icon: Icons.email_outlined,
),
),
const SizedBox(height: 20),
// Password
TextFormField(
validator: (value) {
if (value == null || value.length < 8) {
return "La contraseña debe tener al menos 8 caracteres";
}
return null;
},
onChanged: (value) =>
registerFormProvider.password = value,
obscureText: true,
decoration: CustomInputs.loginInputDecoration(
hint: 'Ingresa tu contraseña',
label: 'Contraseña',
icon: Icons.lock_outline,
),
),
const SizedBox(height: 20),
CustomOutlinedButton(
onPressed: () async {
final validForm = registerFormProvider.validateForm();
if (!validForm) return;
await authProvider.register(
registerFormProvider.email,
registerFormProvider.password,
registerFormProvider.name,
);
},
text: "Crear cuenta",
color: Colors.blue,
),
const SizedBox(height: 20),
LinkText(
text: "Iniciar sesión",
onPressed: () {
Navigator.pushReplacementNamed(
context, Flurorouter.loginRoute);
},
),
const SizedBox(height: 5),
LinkText(
text: "Entrar con celular",
onPressed: () {
Navigator.pushReplacementNamed(
context, Flurorouter.phoneLoginRoute);
},
),
],
),
),
),
),
);
}),
);
}
}
+492
View File
@@ -0,0 +1,492 @@
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:prosapp_web_app/models/pro_state.dart';
import 'package:prosapp_web_app/models/profession.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/professions_provider.dart';
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
import 'package:provider/provider.dart';
class RequestProfessionalView extends StatefulWidget {
const RequestProfessionalView({super.key});
@override
State<RequestProfessionalView> createState() =>
_RequestProfessionalViewState();
}
class _RequestProfessionalViewState extends State<RequestProfessionalView> {
Usuario? user;
List<Profession> professions = [];
late ProfessionalFormProvider professionalFormProvider;
@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final profileFormProvider =
Provider.of<ProfileFormProvider>(context, listen: false);
professionalFormProvider =
Provider.of<ProfessionalFormProvider>(context, listen: false);
final professionsProvider =
Provider.of<ProfessionsProvider>(context, listen: false);
final proProvider =
Provider.of<ProfessionalProvider>(context, listen: false);
profileFormProvider.user = authProvider.user;
proProvider.getProfessional(authProvider.user!.id).then((value) {
professionalFormProvider.setProfesional(value);
});
setState(() {
professions = professionsProvider.professions;
user = authProvider.user;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth < 900) {
return ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewForm()],
);
} else {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: ListView(
physics: const ClampingScrollPhysics(),
children: const [SizedBox(height: 10), _ProfileViewForm()],
),
);
}
});
}
}
class _ProfileViewForm extends StatefulWidget {
const _ProfileViewForm();
@override
State<_ProfileViewForm> createState() => _ProfileViewFormState();
}
class _ProfileViewFormState extends State<_ProfileViewForm> {
final TextEditingController _specialityController = TextEditingController();
List<String> specializations = [];
void _addItemToList() {
setState(() {
String newItem = _specialityController.text.trim();
if (newItem.isNotEmpty) {
specializations.add(newItem);
_specialityController.clear();
}
});
}
void _removeItemFromList(String item) {
setState(() {
specializations.remove(item);
});
}
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final professionsProvider = Provider.of<ProfessionsProvider>(context);
final professions = professionsProvider.professions;
final user = authProvider.user!;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Consumer<ProfessionalFormProvider>(
builder: (context, professionalFormProvider, child) {
if (professionalFormProvider.profesional == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final professional = professionalFormProvider.profesional;
switch (enumToInt(user.proState)) {
case 0:
return WhiteCard(
title: 'Información profesional',
child: Form(
key: professionalFormProvider.formKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(height: 10),
TextFormField(
initialValue: professional!.identification,
validator: (value) {
if (value == null || value.isEmpty) {
return 'La cedula es obligatoria';
}
if (value.trim().length < 6) {
return 'La cedula debe tener al menos 6 caracteres';
}
return null;
},
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
identification: value);
},
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tu cedula',
label: 'Cedula',
icon: Icons.badge_outlined,
),
),
const SizedBox(height: 10),
ElevatedButton.icon(
onPressed: () async {
try {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
Uint8List? fileBytes = file.bytes;
if (fileBytes != null) {
NotificationsService.showBusyIndicator(context);
final provider =
Provider.of<ProfessionalFormProvider>(
context,
listen: false);
await provider.uploadPdfIdentification(
fileBytes, user.id);
Navigator.pop(context);
}
}
} catch (e) {
print('debugeando $e');
}
},
icon: const Icon(Icons.upload_file),
label: const Text('Cargar pdf de la cedula'),
),
const SizedBox(height: 20),
DropdownButtonFormField(
validator: (value) {
if (value == null) {
return 'La profesión es obligatoria';
}
return null;
},
value: professional.profession == ''
? null
: professional.profession,
decoration: CustomInputs.formInputDecoration(
hint: 'Selecciona tu profesión',
label: 'Profesión',
icon: Icons.work_outline_outlined,
),
items: professions.map((Profession profession) {
return DropdownMenuItem<String>(
value: profession.name,
child: Text(
profession.name,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
),
),
);
}).toList(),
onChanged: (value) {
professionalFormProvider.copyProfesionalWith(
profession: value);
}),
const SizedBox(height: 10),
ElevatedButton.icon(
onPressed: () async {
try {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
Uint8List? fileBytes = file.bytes;
if (fileBytes != null) {
NotificationsService.showBusyIndicator(context);
final provider =
Provider.of<ProfessionalFormProvider>(
context,
listen: false);
await provider.uploadPdfCertificate(
fileBytes, user.id);
Navigator.pop(context);
}
}
} catch (e) {
print('debugeando $e');
}
},
icon: const Icon(Icons.upload_file),
label: const Text('Cargar pdf del certificado'),
),
const SizedBox(height: 20),
TextFormField(
validator: (value) {
if (RegExp(r'\s{2,}').hasMatch(value!)) {
return 'La especialización no es valida';
}
return null;
},
onFieldSubmitted: (_) {
_addItemToList();
},
controller: _specialityController,
decoration: CustomInputs.formInputDecoration(
hint: 'Ingresa tus especializaciones y agregalas (+)',
label: 'Especializaciones',
icon: Icons.assignment_outlined,
iconButton: IconButton(
onPressed: () {
_addItemToList();
},
icon: const Icon(Icons.add),
),
),
),
const SizedBox(height: 10),
ElevatedButton.icon(
onPressed: () async {
try {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowMultiple: true,
allowedExtensions: ['pdf'],
withData: true,
);
if (result != null) {
List<Uint8List> filesBytes = result.files
.where((file) => file.bytes != null)
.map((file) => file.bytes!)
.toList();
if (filesBytes.isNotEmpty) {
NotificationsService.showBusyIndicator(context);
final provider =
Provider.of<ProfessionalFormProvider>(
context,
listen: false);
await provider.uploadPdfSpecializations(
filesBytes, user.id);
Navigator.pop(context);
}
}
} catch (e) {
print('debugeando $e');
}
},
icon: const Icon(Icons.upload_file),
label:
const Text('Cargar pdfs de las especializaciones'),
),
const SizedBox(height: 10),
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: specializations
.map((item) => Chip(
label: Text(item),
backgroundColor: Colors.blue.withOpacity(0.3),
labelStyle:
const TextStyle(color: Colors.blue),
deleteIconColor: Colors.blue,
onDeleted: () {
_removeItemFromList(item);
},
shape: RoundedRectangleBorder(
side: const BorderSide(
color: Colors.blue, width: 0.3),
borderRadius: BorderRadius.circular(8),
),
))
.toList(),
),
const SizedBox(height: 20),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: ElevatedButton(
onPressed: () async {
professionalFormProvider.copyProfesionalWith(
specializations: specializations);
final res = await professionalFormProvider
.updateProfesionalInfo(user.id);
if (res) {
Provider.of<AuthProvider>(context,
listen: false)
.refreshUser();
final profileFormProvider =
Provider.of<ProfileFormProvider>(context,
listen: false);
profileFormProvider.copyUserWith(
proState: ProState.pending);
profileFormProvider.updateUserInfoNoValid();
}
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(5)),
),
),
shadowColor:
WidgetStateProperty.all(Colors.transparent),
),
child: const Text(
'Enviar a revisión',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
),
),
),
const SizedBox(height: 15),
],
),
),
);
case 1:
return LayoutBuilder(
builder: (context, constraints) {
double screenWidth = constraints.maxWidth;
double baseFontSize = 18;
double responsiveFontSize =
screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize;
return WhiteCard(
child: Column(
children: [
const Center(
child: Image(
image: AssetImage('checklist.gif'),
width: 320,
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1020),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
'Gracias por proporcionar tu información. Actualmente, estamos revisando tus datos y una vez aprobados, podrás acceder al perfil profesional sin problemas. Te notificaremos tan pronto como tu cuenta esté lista.',
style: TextStyle(fontSize: responsiveFontSize),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 20),
Text(
'¡Gracias por tu paciencia!',
style: TextStyle(fontSize: responsiveFontSize),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
],
),
);
},
);
case 3:
return LayoutBuilder(
builder: (context, constraints) {
double screenWidth = constraints.maxWidth;
double baseFontSize = 18;
double responsiveFontSize =
screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize;
return WhiteCard(
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 25),
child: const Center(
child: Icon(
Icons.sentiment_dissatisfied_outlined,
size: 100,
color: Colors.red,
),
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1020),
child: Container(
margin: const EdgeInsets.only(
left: 8, right: 8, bottom: 10),
child: Text(
'Lamentablemente, tu solicitud no ha sido aceptada en esta ocasión. Por favor, revisa tus datos y vuelve a intentarlo más tarde.',
style: TextStyle(fontSize: responsiveFontSize),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 20),
Text(
'¡Gracias por tu paciencia!',
style: TextStyle(fontSize: responsiveFontSize),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
],
),
);
},
);
}
return Center(child: Text('No se encontró la sección.'));
// return const NoPageFoundView();
}),
),
);
}
}
+577
View File
@@ -0,0 +1,577 @@
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/shared/widgets/schedule_day_tile.dart';
import 'package:provider/provider.dart';
class ScheduleView extends StatefulWidget {
const ScheduleView({super.key});
@override
State<ScheduleView> createState() => _ScheduleViewState();
}
class _ScheduleViewState extends State<ScheduleView> {
Usuario? user;
late ProfessionalFormProvider professionalFormProvider;
@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final profileFormProvider =
Provider.of<ProfileFormProvider>(context, listen: false);
professionalFormProvider =
Provider.of<ProfessionalFormProvider>(context, listen: false);
final proProvider =
Provider.of<ProfessionalProvider>(context, listen: false);
profileFormProvider.user = authProvider.user;
proProvider.getProfessional(authProvider.user!.id).then((value) {
professionalFormProvider.setProfesional(value);
});
profileFormProvider.user = authProvider.user;
setState(() {
user = authProvider.user;
});
}
bool isTimeRangeValid(TimeOfDay startTime, TimeOfDay endTime) {
final start = DateTime(2024, 1, 1, startTime.hour, startTime.minute);
final end = DateTime(2024, 1, 1, endTime.hour, endTime.minute);
return start.isBefore(end);
}
@override
Widget build(BuildContext context) {
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
final user = profileFormProvider.user!;
return Consumer<ProfessionalFormProvider>(
builder: (context, professionalFormProvider, child) {
if (professionalFormProvider.profesional == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final profesional = professionalFormProvider.profesional!;
return ListView(
physics: const ClampingScrollPhysics(),
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
title: 'Horario',
child: Column(
children: [
ScheduleDayTile(
day: 'Lunes',
schedule: profesional.schedules.monday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
monday: profesional.schedules.monday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Martes',
schedule: profesional.schedules.tuesday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
tuesday: profesional.schedules.tuesday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Miercoles',
schedule: profesional.schedules.wednesday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday:
profesional.schedules.wednesday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday:
profesional.schedules.wednesday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday: profesional.schedules.wednesday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday: profesional.schedules.wednesday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday: profesional.schedules.wednesday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
wednesday: profesional.schedules.wednesday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Jueves',
schedule: profesional.schedules.thursday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
thursday: profesional.schedules.thursday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Viernes',
schedule: profesional.schedules.friday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
friday: profesional.schedules.friday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Sabado',
schedule: profesional.schedules.saturday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
saturday: profesional.schedules.saturday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
ScheduleDayTile(
day: 'Domingo',
schedule: profesional.schedules.sunday,
onEnableChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
enabled: value,
),
),
);
});
},
onContinuousDayChanged: (value) {
setState(() {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
continuousDay: value,
),
),
);
});
},
onRange1Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
range1Hour1: pickedTime,
),
),
);
},
onRange1Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
range1Hour2: pickedTime,
),
),
);
},
onRange2Hour1Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
range2Hour1: pickedTime,
),
),
);
},
onRange2Hour2Pick: (pickedTime) {
professionalFormProvider.copyProfesionalWith(
schedules: profesional.schedules.copyWith(
sunday: profesional.schedules.sunday.copyWith(
range2Hour2: pickedTime,
),
),
);
},
),
const Divider(
height: 20,
),
Container(
margin: const EdgeInsets.only(top: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () async {
await professionalFormProvider.updateProfesionalProfileScheduleInfo(user.id);
NavigationService.replaceTo(Flurorouter.professionalProfileRoute);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(5)),
)),
shadowColor:
WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Guardar',
style: TextStyle(color: Colors.white)),
),
),
),
const SizedBox(height: 10),
],
),
),
),
),
],
);
});
}
}
+967
View File
@@ -0,0 +1,967 @@
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_location_preferences.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/providers/settings_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:prosapp_web_app/utils/local_notifications.dart';
class ServiceView extends StatelessWidget {
final String type;
final String serviceId;
const ServiceView({
super.key,
required this.type,
required this.serviceId,
});
@override
Widget build(BuildContext context) {
final settingsProvider = Provider.of<SettingsProvider>(context);
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
if (type == 'user') {
servicesProvider.getServiceForUser(serviceId);
}
if (type == 'professional') {
servicesProvider.getServiceForProfessional(serviceId);
}
return Consumer<ServicesProvider>(
builder: (context, servicesProvider, child) {
if (servicesProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (servicesProvider.service == null) {
// return const NoPageFoundView();
return Center(child: Text('No se encontró el servicio.'));
}
final service = servicesProvider.service!.service;
final user = servicesProvider.service!.user;
final image = (user.picture == '' || user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: user.picture!,
);
return ListView(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: WhiteCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 100,
height: 100,
child: ClipOval(child: image),
),
const SizedBox(height: 16.0),
Text(
user.name.toUpperCase(),
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 5.0),
Text(
'${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}',
style: const TextStyle(
fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 10.0),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 20,
color: Colors.black54,
),
const SizedBox(width: 10),
Text(
_CustomServiceLocation(service.location),
style: const TextStyle(
fontSize: 16, color: Colors.black54),
),
],
),
),
if (service.description.isNotEmpty) ...[
const SizedBox(height: 12.0),
Text(
'"${service.description.trim()}"',
style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 20.0),
customActionButtons(service, user),
const SizedBox(height: 40.0),
customMessageStatus(service, user),
const SizedBox(height: 20.0),
customButton(
service, context, user, servicesProvider),
],
),
),
),
],
),
),
),
),
],
);
});
}
void navigateTo(String routeName) {
NavigationService.replaceTo(routeName);
SideMenuProvider.closeMenu();
}
Widget customActionButtons(Service service, Usuario user) {
DateTime serviceDate = DateTime.parse(service.day);
DateTime now = DateTime.now();
DateTime serviceDateTime = DateTime(
serviceDate.year,
serviceDate.month,
serviceDate.day,
service.range1Hour2.hour,
service.range1Hour2.minute,
);
if (service.status == ServiceStatus.acepted ||
service.status == ServiceStatus.active) {
if (now.isAfter(serviceDateTime)) {
return const SizedBox();
} else {
return Wrap(
alignment: WrapAlignment.center,
spacing: 15,
children: [
Visibility(
visible: user.phone == null || user.phone == '' ? false : true,
child: ElevatedButton(
onPressed: () => launch("tel:${user.phone}"),
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
shape: const CircleBorder(
side: BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0),
child: Icon(
Icons.phone_android,
size: 30,
color: Color(0xFF2BA4EC),
),
),
),
),
ElevatedButton(
onPressed: () {
if (type == 'user') {
NavigationService.navigateTo(
'/dashboard/user/service/${service.id}/chat/${user.id}');
}
if (type == 'professional') {
NavigationService.navigateTo(
'/dashboard/professional/service/${service.id}/chat/${user.id}');
}
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
shape: const CircleBorder(
side: BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0),
child: Icon(
Icons.message,
size: 30,
color: Color(0xFF2BA4EC),
),
),
),
],
);
}
}
return const SizedBox();
}
Widget customMessageStatus(Service service, Usuario user) {
DateTime serviceDate = DateTime.parse(service.day);
DateTime now = DateTime.now();
DateTime serviceDateTime = DateTime(
serviceDate.year,
serviceDate.month,
serviceDate.day,
service.range1Hour2.hour,
service.range1Hour2.minute,
);
if (type == 'user') {
if (service.status == ServiceStatus.completed &&
service.professionalScored == false) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.green.shade50,
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
children: [
const Text(
'Completado',
style: TextStyle(fontSize: 32, color: Colors.green),
),
const Text(
'Califica el servicio',
style: TextStyle(fontSize: 20, color: Colors.green),
),
const SizedBox(height: 15),
FilledButton(
onPressed: () {
NavigationService.navigateTo(
'/dashboard/user/service/${service.id}/rating/${user.id}');
},
style: FilledButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Container(
alignment: Alignment.center,
child: const Text(
'Calificar',
style: TextStyle(
color: Colors.white,
fontSize: 18,
),
),
),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.green.shade50, width: 4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.star,
color: Colors.green,
size: 48,
),
),
)
],
);
}
if (service.status == ServiceStatus.completed &&
service.professionalScored == true) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.green.shade50,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
children: [
Text(
'Completado',
style: TextStyle(fontSize: 32, color: Colors.green),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.green.shade50, width: 4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.star,
color: Colors.green,
size: 48,
),
),
)
],
);
}
if (service.status == ServiceStatus.cancelled) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Text(
'Cancelado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
if (service.status == ServiceStatus.denied) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Text(
'Rechazado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
if (now.isAfter(serviceDateTime)) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Caducado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
Text(
'Tu servicio ha excedido \n el tiempo de espera',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.red),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
}
if (type == 'professional') {
if (service.status == ServiceStatus.completed &&
service.userScored == false) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.green.shade50,
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
children: [
const Text(
'Completado',
style: TextStyle(fontSize: 32, color: Colors.green),
),
const Text(
'Califica el servicio',
style: TextStyle(fontSize: 20, color: Colors.green),
),
const SizedBox(height: 15),
FilledButton(
onPressed: () {
NavigationService.navigateTo(
'/dashboard/professional/service/${service.id}/rating/${user.id}');
},
style: FilledButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Container(
alignment: Alignment.center,
child: const Text(
'Calificar',
style: TextStyle(
color: Colors.white,
fontSize: 18,
),
),
),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.green.shade50, width: 4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.star,
color: Colors.green,
size: 48,
),
),
)
],
);
}
if (service.status == ServiceStatus.completed &&
service.userScored == true) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.green.shade50,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
children: [
Text(
'Completado',
style: TextStyle(fontSize: 32, color: Colors.green),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.green.shade50, width: 4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.star,
color: Colors.green,
size: 48,
),
),
)
],
);
}
if (service.status == ServiceStatus.cancelled) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Text(
'Cancelado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
if (service.status == ServiceStatus.denied) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Text(
'Rechazado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
if (now.isAfter(serviceDateTime)) {
return Stack(
alignment: AlignmentDirectional.topCenter,
clipBehavior: Clip.none,
children: [
Card(
color: Colors.red.shade100,
child: const Padding(
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Caducado',
style: TextStyle(fontSize: 32, color: Colors.red),
),
Text(
'Tu servicio ha excedido \n el tiempo de espera',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.red),
),
],
),
),
),
Positioned(
top: -40,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red.shade100, width: 4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close_rounded,
color: Colors.red, size: 48),
),
)
],
);
}
}
return const SizedBox();
}
Widget customButton(Service service, BuildContext context, Usuario userInfo,
ServicesProvider servicesProvider) {
DateTime serviceDate = DateTime.parse(service.day);
DateTime now = DateTime.now();
DateTime serviceDateTime = DateTime(
serviceDate.year,
serviceDate.month,
serviceDate.day,
service.range1Hour2.hour,
service.range1Hour2.minute,
);
if (now.isAfter(serviceDateTime)) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {
navigateTo(Flurorouter.dashboardRoute);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Volver', style: TextStyle(color: Colors.white)),
),
);
} else {
if (type == 'professional') {
if (service.status == ServiceStatus.pending) {
return Column(
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 160),
child: ElevatedButton(
onPressed: () async {
await servicesProvider.changeServiceStatus(
service.id!, ServiceStatus.denied);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.red.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Rechazar servicio',
style: TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 160),
child: ElevatedButton(
onPressed: () {
servicesProvider.changeServiceStatus(
service.id!, ServiceStatus.acepted);
navigateTo(Flurorouter.dashboardRoute);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Aceptar servicio',
style: TextStyle(color: Colors.white)),
),
),
],
);
}
if (service.status == ServiceStatus.acepted) {
if (serviceDateTime.difference(now).inDays > 1) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {
servicesProvider.changeServiceStatus(
service.id!, ServiceStatus.denied);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.red.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Cancelar servicio',
style: TextStyle(color: Colors.white)),
),
);
} else {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 160),
child: ElevatedButton(
onPressed: () {
servicesProvider.changeServiceStatus(
service.id!, ServiceStatus.active);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Iniciar servicio',
style: TextStyle(color: Colors.white)),
),
);
}
}
if (service.status == ServiceStatus.active) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 160),
child: ElevatedButton(
onPressed: () {
servicesProvider.changeServiceStatus(
service.id!, ServiceStatus.completed);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Terminar servicio',
style: TextStyle(color: Colors.white)),
),
);
}
}
if (type == 'user') {
if (service.status == ServiceStatus.pending) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.red.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Cancelar servicio',
style: TextStyle(color: Colors.white)),
),
);
}
if (service.status == ServiceStatus.acepted) {
if (serviceDateTime.difference(now).inDays > 1) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.red.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Cancelar servicio',
style: TextStyle(color: Colors.white)),
),
);
}
}
if (service.status == ServiceStatus.active) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Terminar servicio',
style: TextStyle(color: Colors.white)),
),
);
// GeneralSecondaryButton(
// label: 'Terminar servicio',
// onPressed: () {
// // final currentState = context.read<ServiceBloc>().state;
// // if (currentState is ServiceLoaded) {
// // context.read<ServiceBloc>().add(UpdateServiceStatus(
// // widget.serviceId, ServiceStatus.completed));
// // }
// });
}
}
}
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130),
child: ElevatedButton(
onPressed: () {
if (type == 'professional') {
navigateTo(Flurorouter.professionalServicesRequestsRoute);
}
if (type == 'user') {
navigateTo(Flurorouter.dashboardRoute);
}
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.blue.shade400,
),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
)),
shadowColor: WidgetStateProperty.all(Colors.transparent),
),
child: const Text('Volver', style: TextStyle(color: Colors.white)),
),
);
}
String _CustomServiceLocation(ServiceLocationPreferences location) {
if (type == 'user') {
if (location == ServiceLocationPreferences.office) {
return 'Servicio en sitio / consultorio';
}
}
if (type == 'professional') {
if (location == ServiceLocationPreferences.office) {
return 'Servicio en tu consultorio';
}
}
if (location == ServiceLocationPreferences.delivery) {
return 'Servicio a domicilio';
}
return 'Error al cargar el servicio.';
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
import 'package:provider/provider.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
class ServicesHistoryView extends StatelessWidget {
final String type;
const ServicesHistoryView({super.key, required this.type});
@override
Widget build(BuildContext context) {
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
if (type == 'user') {
servicesProvider.getServicesHistoryForUser(
Provider.of<AuthProvider>(context, listen: false).user!.id);
}
if (type == 'professional') {
servicesProvider.getServicesHistoryForProfessional(
Provider.of<AuthProvider>(context, listen: false).user!.id);
}
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Consumer<ServicesProvider>(
builder: (context, servicesProvider, child) {
if (servicesProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (servicesProvider.services.isEmpty) {
return ListView(
children: const [
WhiteCard(
child: Center(child: Text('No hay servicios disponibles.')),
),
],
);
}
return ListView.builder(
itemCount: servicesProvider.services.length,
itemBuilder: (context, index) {
final data = servicesProvider.services[index];
final image =
(data.user.picture == '' || data.user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: data.user.picture!,
);
return Container(
margin: const EdgeInsets.only(bottom: 10),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
NavigationService.replaceTo(
'/dashboard/$type/service/${data.service.id}');
},
child: WhiteCard(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: SizedBox(
width: 80,
height: 80,
child: ClipOval(
child: image,
),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
data.user.name,
style: CustomLabels.h2,
),
if (data.service.description != '')
Text(
'"${data.service.description}"',
style: CustomLabels.h5,
),
],
),
),
Padding(
padding: const EdgeInsets.only(right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
style: const TextStyle(
color: Colors.black54, fontSize: 16),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10),
child: customStatus(data.service),
),
],
),
),
],
),
),
),
),
);
},
);
},
),
),
);
}
}
Widget customStatus(Service service) {
if (service.status == ServiceStatus.completed) {
return const StatusItem(text: 'Completado', color: Colors.blueAccent);
}
if (service.status == ServiceStatus.cancelled) {
return const StatusItem(text: 'Cancelado', color: Colors.red);
}
if (service.status == ServiceStatus.denied) {
return const StatusItem(text: 'Rechazado', color: Colors.red);
}
return const SizedBox();
}
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
import 'package:provider/provider.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
class ServicesRequestsView extends StatelessWidget {
const ServicesRequestsView({super.key});
@override
Widget build(BuildContext context) {
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
servicesProvider.getServicesRequestsForProfessional(
Provider.of<AuthProvider>(context, listen: false).user!.id);
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Consumer<ServicesProvider>(
builder: (context, servicesProvider, child) {
if (servicesProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (servicesProvider.services.isEmpty) {
return ListView(
children: const [
WhiteCard(
child: Center(child: Text('No hay servicios disponibles.')),
),
],
);
}
return ListView.builder(
itemCount: servicesProvider.services.length,
itemBuilder: (context, index) {
final data = servicesProvider.services[index];
final image =
(data.user.picture == '' || data.user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: data.user.picture!,
);
return Container(
margin: const EdgeInsets.only(bottom: 10),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
NavigationService.replaceTo(
'/dashboard/professional/service/${data.service.id}');
},
child: WhiteCard(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: SizedBox(
width: 80,
height: 80,
child: ClipOval(
child: image,
),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
data.user.name,
style: CustomLabels.h2,
),
if (data.service.description != '')
Text(
'"${data.service.description}"',
style: CustomLabels.h5,
),
],
),
),
Padding(
padding: const EdgeInsets.only(right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
style: const TextStyle(
color: Colors.black54, fontSize: 16),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10),
child: customStatus(data.service),
),
],
),
),
],
),
),
),
),
);
},
);
},
),
),
);
}
}
Widget customStatus(Service service) {
if (service.status == ServiceStatus.pending) {
return const StatusItem(text: 'Solicitud', color: Colors.black45);
}
return const SizedBox();
}
+155
View File
@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosapp_web_app/models/schedules_entity.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
import 'package:provider/provider.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
class ServicesView extends StatelessWidget {
final String type;
const ServicesView({super.key, required this.type});
@override
Widget build(BuildContext context) {
final servicesProvider =
Provider.of<ServicesProvider>(context, listen: false);
if (type == 'user') {
servicesProvider.getServicesForUser(
Provider.of<AuthProvider>(context, listen: false).user!.id);
}
if (type == 'professional') {
servicesProvider.getServicesForProfessional(
Provider.of<AuthProvider>(context, listen: false).user!.id);
}
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 900),
child: Consumer<ServicesProvider>(
builder: (context, servicesProvider, child) {
if (servicesProvider.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (servicesProvider.services.isEmpty) {
return ListView(
children: const [
WhiteCard(
child: Center(child: Text('No hay servicios disponibles.')),
),
],
);
}
return ListView.builder(
itemCount: servicesProvider.services.length,
itemBuilder: (context, index) {
final data = servicesProvider.services[index];
final image =
(data.user.picture == '' || data.user.picture == null)
? const Image(image: AssetImage('no-image.jpg'))
: FadeInImage.assetNetwork(
placeholder: 'loader.gif',
fit: BoxFit.cover,
image: data.user.picture!,
);
return Container(
margin: const EdgeInsets.only(bottom: 10),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
NavigationService.replaceTo(
'/dashboard/$type/service/${data.service.id}');
},
child: WhiteCard(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: SizedBox(
width: 80,
height: 80,
child: ClipOval(
child: image,
),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
data.user.name,
style: CustomLabels.h2,
),
if (data.service.description != '')
Text(
'"${data.service.description}"',
style: CustomLabels.h5,
),
],
),
),
Padding(
padding: const EdgeInsets.only(right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
style: const TextStyle(
color: Colors.black54, fontSize: 16),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10),
child: customStatus(data.service),
),
],
),
),
],
),
),
),
),
);
},
);
},
),
),
);
}
}
Widget customStatus(Service service) {
if (service.status == ServiceStatus.pending) {
return const StatusItem(text: 'Pendiente', color: Colors.black54);
}
if (service.status == ServiceStatus.acepted) {
return const StatusItem(text: 'Aceptado', color: Colors.green);
}
if (service.status == ServiceStatus.active) {
return const StatusItem(text: 'Activo', color: Colors.blueAccent);
}
return const SizedBox();
}
+211
View File
@@ -0,0 +1,211 @@
import 'package:prosapp_web_app/providers/settings_provider.dart';
import 'package:prosapp_web_app/ui/cards/white_card.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class SupportView extends StatelessWidget {
const SupportView({super.key});
Future<void> _launchURL(String url) async {
final Uri _url = Uri.parse(url);
if (await canLaunchUrl(_url)) {
await launchUrl(
_url,
mode: LaunchMode.externalApplication,
webOnlyWindowName: '_blank', // Para abrir en una nueva pestaña en web
);
} else {
throw 'No se pudo abrir la URL $url';
}
}
Future<void> _goWhatssApp(String number) async {
final url = 'https://api.whatsapp.com/send?phone=$number&text=Hola%21+soy+usuario+de+Prosapp+y+quisiera+conocer+mas+sobre+esta+app+%F0%9F%98%81';
await _launchURL(url);
}
Future<void> _goEmail(String email) async {
final url = 'mailto:$email?subject=${Uri.encodeComponent(email)}';
await _launchURL(url);
}
Future<void> _goSugerencias() async {
const url = 'https://admin.prosapp.co/sugerencias';
await _launchURL(url);
}
Future<void> _goPoliticas(String urlText) async {
final url = urlText;
await _launchURL(url);
}
Future<void> _goTerminos(String urlText) async {
final url = urlText;
await _launchURL(url);
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
if (settingsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final settings = settingsProvider.settings!;
return ListView(
physics: const ClampingScrollPhysics(),
children: [
const SizedBox(height: 10),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: WhiteCard(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 30),
child: Center(
child: Text(
settings.tituloSoporte,
style: TextStyle(
fontSize: size.width > 500 ? 30 : 20,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
),
Padding(
padding: EdgeInsets.only(
left: size.width > 500 ? 30 : 0,
right: size.width > 500 ? 30 : 0,
bottom: 30,
),
child: Text(
settings.parrafoSoporte,
style: CustomLabels.h3,
textAlign: TextAlign.center,
),
),
Center(
child: Column(
children: [
const Text(
'Horario de atención:',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 10),
Text(settings.diasSoporte, style: CustomLabels.h3),
Text(settings.horasSoporte, style: CustomLabels.h3),
],
),
),
const SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(
left: size.width > 500 ? 30 : 0,
right: size.width > 500 ? 30 : 0,
bottom: 30,
),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 15,
runSpacing: 15,
children: [
_buildGridButton(
context,
icon: Icons.phone,
text: 'WhatsApp',
onPressed: () =>
_goWhatssApp(settings.numeroSoporte),
),
_buildGridButton(
context,
icon: Icons.email,
text: 'Email',
onPressed: () => _goEmail(settings.emailSoporte),
),
_buildGridButton(
context,
icon: Icons.feedback,
text: 'Sugerencias',
onPressed: _goSugerencias,
),
_buildGridButton(
context,
icon: Icons.privacy_tip,
text: 'Políticas de Privacidad',
onPressed: () =>
_goPoliticas(settings.politicasPrivacidad),
),
_buildGridButton(
context,
icon: Icons.article,
text: 'Términos y Condiciones',
onPressed: () =>
_goTerminos(settings.terminosCondiciones),
),
],
),
),
],
),
),
),
),
],
);
},
);
}
Widget _buildGridButton(
BuildContext context, {
required IconData icon,
required String text,
required VoidCallback onPressed,
}) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onPressed,
child: Container(
height: 110,
width: 180,
decoration: BoxDecoration(
color: Colors.blue[600],
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40, color: Colors.white),
const SizedBox(height: 10),
Center(
child: Text(
textAlign: TextAlign.center,
text,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
),
),
),
],
),
),
),
);
}
}