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
+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),
),
);
}
}