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) => { const updateMonthlyDraw = async (req, res) => {
try { try {
const { draw_id } = req.params; 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; const managerId = req.user.id;
// Find the draw // Find the draw
@ -664,9 +664,64 @@ const updateMonthlyDraw = async (req, res) => {
const updates = {}; const updates = {};
// Update winner if provided 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 (monthProvided || yearProvided) {
if (Number.isNaN(nextMonth) || nextMonth < 1 || nextMonth > 12) {
return res.status(400).json({
success: false,
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;
}
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) { if (winner_id) {
// Verify the winner is a member of the group const winnerStr = String(winner_id);
const currentWinnerStr = monthlyDraw.winner_id != null ? String(monthlyDraw.winner_id) : '';
if (winnerStr !== currentWinnerStr) {
const member = await GroupMember.findOne({ const member = await GroupMember.findOne({
where: { where: {
group_id: monthlyDraw.ChitGroup.id, group_id: monthlyDraw.ChitGroup.id,
@ -683,22 +738,46 @@ const updateMonthlyDraw = async (req, res) => {
}); });
} }
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; updates.winner_id = winner_id;
if (!notes) { if (notes === undefined || notes === null) {
updates.notes = `Winner updated to: ${member.User.full_name}`; updates.notes = `Winner updated to: ${member.User.full_name}`;
} }
} }
}
// Update prize amount if provided // Update prize amount if provided (including 0)
if (prize_amount) { if (prize_amount != null && prize_amount !== '') {
updates.prize_amount = prize_amount; updates.prize_amount = prize_amount;
} }
// Update notes if provided // Update notes if provided
if (notes) { if (notes !== undefined && notes !== null) {
updates.notes = notes; updates.notes = notes;
} }
if (Object.keys(updates).length === 0) {
return res.status(400).json({
success: false,
message: 'No valid fields to update'
});
}
// Perform update // Perform update
await monthlyDraw.update(updates); await monthlyDraw.update(updates);

View File

@ -584,6 +584,11 @@ class ApiService {
if (responseData is Map<String, dynamic>) { if (responseData is Map<String, dynamic>) {
message = responseData['message']?.toString() ?? 'Request failed'; 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) { } else if (responseData is String) {
message = responseData; message = responseData;
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../core/services/chit_group_service.dart'; import '../../core/services/chit_group_service.dart';
import '../../core/services/api_service.dart'; import '../../core/services/api_service.dart';
import '../../core/models/monthly_draw.dart'; import '../../core/models/monthly_draw.dart';
@ -26,6 +27,33 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
String? _selectedMemberId; String? _selectedMemberId;
bool _isLoading = false; 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 @override
void initState() { void initState() {
@ -33,6 +61,9 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
_selectedMemberId = widget.draw.winnerId; _selectedMemberId = widget.draw.winnerId;
_prizeAmountController.text = widget.draw.prizeAmount.toStringAsFixed(0); _prizeAmountController.text = widget.draw.prizeAmount.toStringAsFixed(0);
_notesController.text = widget.draw.notes ?? ''; _notesController.text = widget.draw.notes ?? '';
_editMonth = widget.draw.month.clamp(1, 12);
_editYear = _clampEditYear(widget.draw.year);
_drawDate = _baselineDrawDate();
} }
@override @override
@ -42,6 +73,18 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
super.dispose(); 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 { Future<void> _handleSubmit() async {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
if (_selectedMemberId == null) { if (_selectedMemberId == null) {
@ -54,7 +97,16 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
try { try {
final updates = <String, dynamic>{}; 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) { if (_selectedMemberId != widget.draw.winnerId) {
updates['winner_id'] = _selectedMemberId; updates['winner_id'] = _selectedMemberId;
} }
@ -64,7 +116,7 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
updates['prize_amount'] = newPrizeAmount; updates['prize_amount'] = newPrizeAmount;
} }
if (_notesController.text.isNotEmpty && _notesController.text != widget.draw.notes) { if (_notesController.text != (widget.draw.notes ?? '')) {
updates['notes'] = _notesController.text; updates['notes'] = _notesController.text;
} }
@ -82,12 +134,22 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
title: 'Success', title: 'Success',
); );
Get.back(result: true); Get.back(result: true);
} else {
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 { } else {
SnackbarUtil.showError( SnackbarUtil.showError(
response['message'] ?? 'Failed to update draw', response['message'] ?? 'Failed to update draw',
title: 'Error', title: 'Error',
); );
} }
}
} catch (e) { } catch (e) {
SnackbarUtil.showError('Error: ${e.toString()}'); SnackbarUtil.showError('Error: ${e.toString()}');
} finally { } finally {
@ -137,7 +199,7 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
), ),
), ),
Text( Text(
'Month ${widget.draw.month}/${widget.draw.year}', 'Originally ${widget.draw.month}/${widget.draw.year}',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey.shade600, color: Colors.grey.shade600,
@ -191,6 +253,108 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
), ),
SizedBox(height: 24.h), 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 // Select Winner
Text( Text(
'Winner', '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 set -eo pipefail
# Usage: ./deploy.sh [backend|frontend|--force]
set -e PROJECT_DIR="${LUCKYCHIT_ROOT:-/home/luckychit/apps/chitfund}"
BRANCH="${DEPLOY_BRANCH:-prod_i8n}"
PROJECT_DIR="/home/luckychit/apps/chitfund"
FORCE_REBUILD=false FORCE_REBUILD=false
TARGET=""
# Check for force flag usage() {
if [ "$1" == "--force" ] || [ "$2" == "--force" ]; then 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 FORCE_REBUILD=true
fi ;;
backend|frontend)
cd $PROJECT_DIR if [ -n "$TARGET" ] && [ "$TARGET" != "$arg" ]; then
echo "Error: specify only one of backend or frontend."
echo "🚀 LuckyChit Deployment" usage
echo "======================="
echo ""
# Git pull
echo "📥 Pulling latest code..."
git stash 2>/dev/null || true
git pull origin prod_i8n
echo ""
# 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 fi
TARGET="$arg"
;;
-h|--help)
usage
;;
*)
echo "Unknown argument: $arg"
usage
;;
esac
done
npm install run_backend() {
pm2 restart luckychit-api [ "$TARGET" = "" ] || [ "$TARGET" = "backend" ]
echo "✅ Backend deployed" }
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 "" echo ""
fi fi
# Deploy frontend echo "Pulling latest code..."
if [ "$1" == "" ] || [ "$1" == "frontend" ] || [ "$1" == "--force" ]; then git fetch origin "$BRANCH"
echo "🎨 Deploying Frontend..." if ! git rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then
cd $PROJECT_DIR/luckychit 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 ""
if run_backend; then
echo "Deploying backend..."
cd "$PROJECT_DIR/backend"
if [ "$FORCE_REBUILD" = true ]; then if [ "$FORCE_REBUILD" = true ]; then
echo "🗑️ Force rebuild: Cleaning Flutter cache..." 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
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
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 flutter clean
rm -rf .dart_tool build rm -rf .dart_tool build
fi fi
flutter pub get flutter pub get
BUILD_NUMBER=$(date +%s) if [ -f tool/sync_l10n.mjs ]; then
flutter build web --release --pwa-strategy=none --build-number=$BUILD_NUMBER node tool/sync_l10n.mjs
echo "📦 Build version: $BUILD_NUMBER" fi
pm2 restart luckychit-frontend BUILD_NUMBER=$(date +%s)
echo "✅ Frontend deployed" 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 "" echo ""
fi fi
# Status echo "PM2 status:"
echo "📊 PM2 Status:"
pm2 status pm2 status
echo "" echo ""
echo "✅ Deployment complete!" echo "Deployment complete."
echo "" echo "Smoke tests:"
echo "🧪 Test:" echo " curl -sf http://localhost:3000/health || curl -sf http://localhost:3000/api/health"
echo " Backend: curl http://localhost:3000/health" echo " curl -sfI http://localhost:8080/"
echo " Frontend: curl http://localhost:8080" echo "Logs: pm2 logs --lines 30"
echo " Domain: https://chitfund.deepteklabs.com"
echo ""
echo "📝 Check logs: pm2 logs --lines 20"
echo "💡 Clear browser cache: Ctrl + Shift + R"