From c4439e76a83d091c619b6bdcf769fe1ba598a54c Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Mon, 4 May 2026 23:02:52 -0400 Subject: [PATCH] update draw --- .../src/controllers/monthlyDrawController.js | 119 +++++++++-- luckychit/lib/core/services/api_service.dart | 9 +- .../interfaces/manager/edit_draw_dialog.dart | 178 +++++++++++++++- scripts/deploy.sh | 193 ++++++++++++------ 4 files changed, 407 insertions(+), 92 deletions(-) diff --git a/backend/src/controllers/monthlyDrawController.js b/backend/src/controllers/monthlyDrawController.js index a0e05c6..d0fb955 100644 --- a/backend/src/controllers/monthlyDrawController.js +++ b/backend/src/controllers/monthlyDrawController.js @@ -633,7 +633,7 @@ const deleteMonthlyDraw = async (req, res) => { const updateMonthlyDraw = async (req, res) => { try { const { draw_id } = req.params; - const { winner_id, prize_amount, notes } = req.body; + const { winner_id, prize_amount, notes, month, year, draw_date } = req.body; const managerId = req.user.id; // Find the draw @@ -664,41 +664,120 @@ const updateMonthlyDraw = async (req, res) => { const updates = {}; - // Update winner if provided - if (winner_id) { - // Verify the winner is a member of the group - const member = await GroupMember.findOne({ - where: { - group_id: monthlyDraw.ChitGroup.id, - user_id: winner_id, - status: 'active' - }, - include: [{ model: User, attributes: ['id', 'full_name'] }] - }); + let nextMonth = monthlyDraw.month; + let nextYear = monthlyDraw.year; + const monthProvided = month !== undefined && month !== null && month !== ''; + const yearProvided = year !== undefined && year !== null && year !== ''; + if (monthProvided) { + nextMonth = parseInt(month, 10); + } + if (yearProvided) { + nextYear = parseInt(year, 10); + } - if (!member) { + if (monthProvided || yearProvided) { + if (Number.isNaN(nextMonth) || nextMonth < 1 || nextMonth > 12) { return res.status(400).json({ success: false, - message: 'Winner must be an active member of this group' + message: 'Month must be between 1 and 12' }); } + if (Number.isNaN(nextYear) || nextYear < 2020) { + return res.status(400).json({ + success: false, + message: 'Year must be 2020 or later' + }); + } + const conflict = await MonthlyDraw.findOne({ + where: { + group_id: monthlyDraw.group_id, + month: nextMonth, + year: nextYear, + id: { [Op.ne]: draw_id } + } + }); + if (conflict) { + return res.status(400).json({ + success: false, + message: `Another draw already exists for ${nextMonth}/${nextYear}` + }); + } + updates.month = nextMonth; + updates.year = nextYear; + } - updates.winner_id = winner_id; - if (!notes) { - updates.notes = `Winner updated to: ${member.User.full_name}`; + if (draw_date !== undefined && draw_date !== null && draw_date !== '') { + const d = new Date(draw_date); + if (Number.isNaN(d.getTime())) { + return res.status(400).json({ + success: false, + message: 'Invalid draw_date' + }); + } + updates.draw_date = d; + } + + // Update winner if provided and different from current + if (winner_id) { + const winnerStr = String(winner_id); + const currentWinnerStr = monthlyDraw.winner_id != null ? String(monthlyDraw.winner_id) : ''; + if (winnerStr !== currentWinnerStr) { + const member = await GroupMember.findOne({ + where: { + group_id: monthlyDraw.ChitGroup.id, + user_id: winner_id, + status: 'active' + }, + include: [{ model: User, attributes: ['id', 'full_name'] }] + }); + + if (!member) { + return res.status(400).json({ + success: false, + message: 'Winner must be an active member of this group' + }); + } + + const duplicateWinner = await MonthlyDraw.findOne({ + where: { + group_id: monthlyDraw.group_id, + winner_id, + id: { [Op.ne]: draw_id } + } + }); + if (duplicateWinner) { + return res.status(400).json({ + success: false, + message: `${member.User.full_name} has already won in another draw. Each member can only win once.`, + alreadyWon: true, + winnerName: member.User.full_name + }); + } + + updates.winner_id = winner_id; + if (notes === undefined || notes === null) { + updates.notes = `Winner updated to: ${member.User.full_name}`; + } } } - // Update prize amount if provided - if (prize_amount) { + // Update prize amount if provided (including 0) + if (prize_amount != null && prize_amount !== '') { updates.prize_amount = prize_amount; } // Update notes if provided - if (notes) { + if (notes !== undefined && notes !== null) { updates.notes = notes; } + if (Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + message: 'No valid fields to update' + }); + } + // Perform update await monthlyDraw.update(updates); diff --git a/luckychit/lib/core/services/api_service.dart b/luckychit/lib/core/services/api_service.dart index 9b229f7..60e712c 100644 --- a/luckychit/lib/core/services/api_service.dart +++ b/luckychit/lib/core/services/api_service.dart @@ -581,13 +581,18 @@ class ApiService { if (e.response != null) { final responseData = e.response?.data; String message = 'Request failed'; - + if (responseData is Map) { message = responseData['message']?.toString() ?? 'Request failed'; + final merged = Map.from(responseData); + merged['success'] = false; + merged['message'] = message; + merged['statusCode'] = e.response?.statusCode; + return merged; } else if (responseData is String) { message = responseData; } - + return { 'success': false, 'message': message, diff --git a/luckychit/lib/interfaces/manager/edit_draw_dialog.dart b/luckychit/lib/interfaces/manager/edit_draw_dialog.dart index f40c8d9..eb219ac 100644 --- a/luckychit/lib/interfaces/manager/edit_draw_dialog.dart +++ b/luckychit/lib/interfaces/manager/edit_draw_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/intl.dart'; import '../../core/services/chit_group_service.dart'; import '../../core/services/api_service.dart'; import '../../core/models/monthly_draw.dart'; @@ -26,6 +27,33 @@ class _EditDrawDialogState extends State { String? _selectedMemberId; bool _isLoading = false; + late int _editMonth; + late int _editYear; + late DateTime _drawDate; + + static const _monthNames = [ + '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + + DateTime _baselineDrawDate() { + final d = widget.draw.drawDate; + if (d.millisecondsSinceEpoch == 0) { + return DateTime(widget.draw.year, widget.draw.month, 1); + } + return d; + } + + bool _sameCalendarDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + int _clampEditYear(int y) { + final minY = 2020; + final maxY = DateTime.now().year + 1; + if (y < minY) return minY; + if (y > maxY) return maxY; + return y; + } @override void initState() { @@ -33,6 +61,9 @@ class _EditDrawDialogState extends State { _selectedMemberId = widget.draw.winnerId; _prizeAmountController.text = widget.draw.prizeAmount.toStringAsFixed(0); _notesController.text = widget.draw.notes ?? ''; + _editMonth = widget.draw.month.clamp(1, 12); + _editYear = _clampEditYear(widget.draw.year); + _drawDate = _baselineDrawDate(); } @override @@ -42,6 +73,18 @@ class _EditDrawDialogState extends State { super.dispose(); } + Future _pickDrawDate() async { + final picked = await showDatePicker( + context: context, + initialDate: _drawDate, + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365 * 3)), + ); + if (picked != null) { + setState(() => _drawDate = picked); + } + } + Future _handleSubmit() async { if (_formKey.currentState!.validate()) { if (_selectedMemberId == null) { @@ -54,7 +97,16 @@ class _EditDrawDialogState extends State { try { final updates = {}; - // Only include changed fields + if (_editMonth != widget.draw.month) { + updates['month'] = _editMonth; + } + if (_editYear != widget.draw.year) { + updates['year'] = _editYear; + } + if (!_sameCalendarDay(_drawDate, _baselineDrawDate())) { + updates['draw_date'] = _drawDate.toUtc().toIso8601String(); + } + if (_selectedMemberId != widget.draw.winnerId) { updates['winner_id'] = _selectedMemberId; } @@ -64,7 +116,7 @@ class _EditDrawDialogState extends State { updates['prize_amount'] = newPrizeAmount; } - if (_notesController.text.isNotEmpty && _notesController.text != widget.draw.notes) { + if (_notesController.text != (widget.draw.notes ?? '')) { updates['notes'] = _notesController.text; } @@ -83,10 +135,20 @@ class _EditDrawDialogState extends State { ); Get.back(result: true); } else { - SnackbarUtil.showError( - response['message'] ?? 'Failed to update draw', - title: 'Error', - ); + final isAlreadyWon = response['alreadyWon'] ?? false; + final winnerName = response['winnerName'] ?? ''; + if (isAlreadyWon && winnerName.isNotEmpty) { + SnackbarUtil.showError( + '$winnerName has already won in another draw.\nEach member can only win once.', + title: 'Duplicate winner', + duration: const Duration(seconds: 4), + ); + } else { + SnackbarUtil.showError( + response['message'] ?? 'Failed to update draw', + title: 'Error', + ); + } } } catch (e) { SnackbarUtil.showError('Error: ${e.toString()}'); @@ -137,7 +199,7 @@ class _EditDrawDialogState extends State { ), ), Text( - 'Month ${widget.draw.month}/${widget.draw.year}', + 'Originally ${widget.draw.month}/${widget.draw.year}', style: TextStyle( fontSize: 14.sp, color: Colors.grey.shade600, @@ -191,6 +253,108 @@ class _EditDrawDialogState extends State { ), SizedBox(height: 24.h), + Text( + 'Draw month & year', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: _editMonth, + decoration: InputDecoration( + labelText: 'Month', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + items: List.generate(12, (i) { + final m = i + 1; + return DropdownMenuItem( + value: m, + child: Text('${_monthNames[m]} ($m)'), + ); + }), + onChanged: (v) { + if (v != null) setState(() => _editMonth = v); + }, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: DropdownButtonFormField( + value: _editYear, + decoration: InputDecoration( + labelText: 'Year', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + items: List.generate( + DateTime.now().year - 2019 + 2, + (i) { + final y = 2020 + i; + return DropdownMenuItem( + value: y, + child: Text('$y'), + ); + }, + ), + onChanged: (v) { + if (v != null) setState(() => _editYear = v); + }, + ), + ), + ], + ), + SizedBox(height: 8.h), + Text( + 'This is the calendar month this draw belongs to. It must not clash with another draw in this group.', + style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600), + ), + SizedBox(height: 20.h), + + Text( + 'Draw date', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + InkWell( + onTap: _pickDrawDate, + borderRadius: BorderRadius.circular(12.r), + child: InputDecorator( + decoration: InputDecoration( + prefixIcon: const Icon(Icons.calendar_today_outlined), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + child: Text( + DateFormat('d MMM yyyy').format(_drawDate), + style: TextStyle(fontSize: 16.sp), + ), + ), + ), + SizedBox(height: 8.h), + Text( + 'When the draw actually happened (shown in history).', + style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600), + ), + SizedBox(height: 20.h), + // Select Winner Text( 'Winner', diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 5265482..7e7abfc 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,80 +1,147 @@ -#!/bin/bash +#!/usr/bin/env bash +# +# LuckyChit deployment โ€” backend (Node/PM2) + frontend (Flutter web + PM2). +# +# Usage: +# ./scripts/deploy.sh # backend + frontend +# ./scripts/deploy.sh backend # API only +# ./scripts/deploy.sh frontend # Flutter web only +# ./scripts/deploy.sh backend --force # e.g. wipe node_modules / flutter clean +# +# Env (optional): +# LUCKYCHIT_ROOT Repo root (default: /home/luckychit/apps/chitfund) +# DEPLOY_BRANCH Git branch to fast-forward to (default: prod_i8n) -# LuckyChit Unified Deployment Script -# Usage: ./deploy.sh [backend|frontend|--force] +set -eo pipefail -set -e - -PROJECT_DIR="/home/luckychit/apps/chitfund" +PROJECT_DIR="${LUCKYCHIT_ROOT:-/home/luckychit/apps/chitfund}" +BRANCH="${DEPLOY_BRANCH:-prod_i8n}" FORCE_REBUILD=false +TARGET="" -# Check for force flag -if [ "$1" == "--force" ] || [ "$2" == "--force" ]; then - FORCE_REBUILD=true +usage() { + echo "Usage: $0 [backend|frontend] [--force]" + echo " (no args) deploy backend and frontend" + echo " backend deploy Node API only (pm2: luckychit-api)" + echo " frontend build Flutter web + restart static server (pm2: luckychit-web)" + echo " --force aggressive clean (backend: rm node_modules; frontend: flutter clean)" + exit 1 +} + +for arg in "$@"; do + case "$arg" in + --force) + FORCE_REBUILD=true + ;; + backend|frontend) + if [ -n "$TARGET" ] && [ "$TARGET" != "$arg" ]; then + echo "Error: specify only one of backend or frontend." + usage + fi + TARGET="$arg" + ;; + -h|--help) + usage + ;; + *) + echo "Unknown argument: $arg" + usage + ;; + esac +done + +run_backend() { + [ "$TARGET" = "" ] || [ "$TARGET" = "backend" ] +} + +run_frontend() { + [ "$TARGET" = "" ] || [ "$TARGET" = "frontend" ] +} + +cd "$PROJECT_DIR" + +echo "LuckyChit deployment" +echo "====================" +echo "Root: $PROJECT_DIR" +echo "Branch: $BRANCH" +echo "Target: ${TARGET:-backend+frontend} force=$FORCE_REBUILD" +echo "" + +if [ "$(id -u)" -eq 0 ]; then + echo "Warning: running as root. Prefer a normal user so Flutter/npm caches stay consistent." + echo "" fi -cd $PROJECT_DIR - -echo "๐Ÿš€ LuckyChit Deployment" -echo "=======================" +echo "Pulling latest code..." +git fetch origin "$BRANCH" +if ! git rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then + echo "Error: origin/$BRANCH not found. Fetch failed or branch missing on remote." + exit 1 +fi +git checkout "$BRANCH" +git merge --ff-only "origin/$BRANCH" echo "" -# Git pull -echo "๐Ÿ“ฅ Pulling latest code..." -git stash 2>/dev/null || true -git pull origin prod_i8n -echo "" +if run_backend; then + echo "Deploying backend..." + cd "$PROJECT_DIR/backend" -# Deploy backend -if [ "$1" == "" ] || [ "$1" == "backend" ] || [ "$1" == "--force" ]; then - echo "๐Ÿ”ง Deploying Backend..." - cd $PROJECT_DIR/backend - - if [ "$FORCE_REBUILD" = true ]; then - echo "๐Ÿ—‘๏ธ Force rebuild: Removing node_modules..." - rm -rf node_modules package-lock.json - fi - + if [ "$FORCE_REBUILD" = true ]; then + echo "Force rebuild: removing node_modules and lockfile..." + rm -rf node_modules package-lock.json + fi + + if [ -f package-lock.json ]; then + npm ci + else npm install - pm2 restart luckychit-api - echo "โœ… Backend deployed" - echo "" + fi + + if pm2 describe luckychit-api >/dev/null 2>&1; then + pm2 restart luckychit-api --update-env + else + pm2 start ecosystem.config.js --env production + fi + echo "Backend done." + echo "" fi -# Deploy frontend -if [ "$1" == "" ] || [ "$1" == "frontend" ] || [ "$1" == "--force" ]; then - echo "๐ŸŽจ Deploying Frontend..." - cd $PROJECT_DIR/luckychit - - if [ "$FORCE_REBUILD" = true ]; then - echo "๐Ÿ—‘๏ธ Force rebuild: Cleaning Flutter cache..." - flutter clean - rm -rf .dart_tool build - fi - - flutter pub get - - BUILD_NUMBER=$(date +%s) - flutter build web --release --pwa-strategy=none --build-number=$BUILD_NUMBER - echo "๐Ÿ“ฆ Build version: $BUILD_NUMBER" - - pm2 restart luckychit-frontend - echo "โœ… Frontend deployed" - echo "" +if run_frontend; then + echo "Deploying frontend (Flutter web)..." + cd "$PROJECT_DIR/luckychit" + + if [ "$FORCE_REBUILD" = true ]; then + echo "Force rebuild: flutter clean..." + flutter clean + rm -rf .dart_tool build + fi + + flutter pub get + + if [ -f tool/sync_l10n.mjs ]; then + node tool/sync_l10n.mjs + fi + + BUILD_NUMBER=$(date +%s) + flutter build web --release --pwa-strategy=none --build-number="$BUILD_NUMBER" + echo "Build number: $BUILD_NUMBER" + + # Name must match luckychit/ecosystem.config.js (app name: luckychit-web) + if pm2 describe luckychit-web >/dev/null 2>&1; then + pm2 restart luckychit-web --update-env + else + pm2 start ecosystem.config.js --env production + fi + echo "Frontend done." + echo "" fi -# Status -echo "๐Ÿ“Š PM2 Status:" +echo "PM2 status:" pm2 status echo "" -echo "โœ… Deployment complete!" -echo "" -echo "๐Ÿงช Test:" -echo " Backend: curl http://localhost:3000/health" -echo " Frontend: curl http://localhost:8080" -echo " Domain: https://chitfund.deepteklabs.com" -echo "" -echo "๐Ÿ“ Check logs: pm2 logs --lines 20" -echo "๐Ÿ’ก Clear browser cache: Ctrl + Shift + R" - +echo "Deployment complete." +echo "Smoke tests:" +echo " curl -sf http://localhost:3000/health || curl -sf http://localhost:3000/api/health" +echo " curl -sfI http://localhost:8080/" +echo "Logs: pm2 logs --lines 30"