update draw

This commit is contained in:
Deep Koluguri 2026-05-04 23:02:52 -04:00
parent e404721818
commit c4439e76a8
4 changed files with 407 additions and 92 deletions

View File

@ -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);

View File

@ -581,13 +581,18 @@ class ApiService {
if (e.response != null) {
final responseData = e.response?.data;
String message = 'Request failed';
if (responseData is Map<String, dynamic>) {
message = responseData['message']?.toString() ?? 'Request failed';
final merged = Map<String, dynamic>.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,

View File

@ -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<EditDrawDialog> {
String? _selectedMemberId;
bool _isLoading = false;
late int _editMonth;
late int _editYear;
late DateTime _drawDate;
static const _monthNames = <String>[
'', '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<EditDrawDialog> {
_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<EditDrawDialog> {
super.dispose();
}
Future<void> _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<void> _handleSubmit() async {
if (_formKey.currentState!.validate()) {
if (_selectedMemberId == null) {
@ -54,7 +97,16 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
try {
final updates = <String, dynamic>{};
// 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<EditDrawDialog> {
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<EditDrawDialog> {
);
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<EditDrawDialog> {
),
),
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<EditDrawDialog> {
),
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<int>(
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<int>(
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',

View File

@ -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"