Commit 3902c88f authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(members): add photo upload/update/delete for member and all dependents

- Member header photo: camera icon to upload/change, X to delete
- Spouses: hover avatar to upload/delete photo (routes + controller)
- Children: hover avatar to upload/delete photo (routes + controller)
- Temporary members: hover avatar to upload/delete photo (routes + controller)
- All use existing PhotoUploadService with compression + thumbnails
- CSS hover reveals action buttons on dependent avatars
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0233895c
...@@ -400,4 +400,54 @@ class ChildController extends Controller ...@@ -400,4 +400,54 @@ class ChildController extends Controller
$result = ChildFeeCalculator::calculate($memberId, $request->all()); $result = ChildFeeCalculator::calculate($memberId, $request->all());
return $this->json($result); return $this->json($result);
} }
public function uploadPhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$child = $db->selectOne("SELECT * FROM children WHERE id = ? AND member_id = ?", [(int) $id, (int) $memberId]);
if (!$child) {
return $this->redirect('/members/' . $memberId)->withError('الابن/ة غير موجود');
}
$photoFile = $_FILES['photo'] ?? [];
if (!PhotoUploadService::isUploaded($photoFile)) {
return $this->redirect('/members/' . $memberId)->withError('لم يتم رفع صورة');
}
$validation = PhotoUploadService::validate($photoFile);
if (!$validation['valid']) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $validation['errors']));
return $this->redirect('/members/' . $memberId);
}
$result = PhotoUploadService::upload($photoFile, 'children', (int) $id);
if ($result) {
$db->update('children', ['photo_path' => $result['path']], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم رفع صورة الابن/ة بنجاح');
}
return $this->redirect('/members/' . $memberId)->withError('حدث خطأ أثناء حفظ الصورة');
}
public function deletePhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$child = $db->selectOne("SELECT * FROM children WHERE id = ? AND member_id = ?", [(int) $id, (int) $memberId]);
if (!$child) {
return $this->redirect('/members/' . $memberId)->withError('الابن/ة غير موجود');
}
if (empty($child['photo_path'])) {
return $this->redirect('/members/' . $memberId)->withError('لا توجد صورة لحذفها');
}
$fullPath = App::getInstance()->basePath() . '/public/' . $child['photo_path'];
if (is_file($fullPath)) {
@unlink($fullPath);
}
$db->update('children', ['photo_path' => null], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم حذف صورة الابن/ة');
}
} }
\ No newline at end of file
...@@ -9,5 +9,7 @@ return [ ...@@ -9,5 +9,7 @@ return [
['POST', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@update', ['auth', 'csrf'], 'child.edit'], ['POST', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@update', ['auth', 'csrf'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}/archive', 'Children\Controllers\ChildController@archive', ['auth', 'csrf'], 'child.remove'], ['POST', '/members/{memberId}/children/{id}/archive', 'Children\Controllers\ChildController@archive', ['auth', 'csrf'], 'child.remove'],
['POST', '/members/{memberId}/children/{id}/freeze', 'Children\Controllers\ChildController@freeze', ['auth', 'csrf'], 'child.freeze'], ['POST', '/members/{memberId}/children/{id}/freeze', 'Children\Controllers\ChildController@freeze', ['auth', 'csrf'], 'child.freeze'],
['POST', '/members/{memberId}/children/{id}/photo', 'Children\Controllers\ChildController@uploadPhoto', ['auth', 'csrf'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}/photo/delete', 'Children\Controllers\ChildController@deletePhoto', ['auth', 'csrf'], 'child.edit'],
['POST', '/api/children/calculate-fee', 'Children\Controllers\ChildController@calculateFee', ['auth'], 'child.add'], ['POST', '/api/children/calculate-fee', 'Children\Controllers\ChildController@calculateFee', ['auth'], 'child.add'],
]; ];
\ No newline at end of file
...@@ -873,6 +873,26 @@ class MemberController extends Controller ...@@ -873,6 +873,26 @@ class MemberController extends Controller
return $this->redirect('/members/' . $id)->withError('حدث خطأ أثناء حفظ الصورة'); return $this->redirect('/members/' . $id)->withError('حدث خطأ أثناء حفظ الصورة');
} }
public function deletePhoto(Request $request, string $id): Response
{
$member = Member::find((int) $id);
if (!$member || $member->is_archived) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
if (empty($member->photo_path)) {
return $this->redirect('/members/' . $id)->withError('لا توجد صورة لحذفها');
}
$fullPath = App::getInstance()->basePath() . '/public/' . $member->photo_path;
if (is_file($fullPath)) {
@unlink($fullPath);
}
$member->update(['photo_path' => null]);
return $this->redirect('/members/' . $id)->withSuccess('تم حذف الصورة');
}
public function changeStatus(Request $request, string $id): Response public function changeStatus(Request $request, string $id): Response
{ {
$member = Member::find((int) $id); $member = Member::find((int) $id);
......
...@@ -16,6 +16,7 @@ return [ ...@@ -16,6 +16,7 @@ return [
['GET', '/members/{id}/edit', 'Members\Controllers\MemberController@edit', ['auth'], 'member.edit'], ['GET', '/members/{id}/edit', 'Members\Controllers\MemberController@edit', ['auth'], 'member.edit'],
['POST', '/members/{id}', 'Members\Controllers\MemberController@update', ['auth', 'csrf'], 'member.edit'], ['POST', '/members/{id}', 'Members\Controllers\MemberController@update', ['auth', 'csrf'], 'member.edit'],
['POST', '/members/{id}/photo', 'Members\Controllers\MemberController@uploadPhoto', ['auth', 'csrf'], 'member.edit'], ['POST', '/members/{id}/photo', 'Members\Controllers\MemberController@uploadPhoto', ['auth', 'csrf'], 'member.edit'],
['POST', '/members/{id}/photo/delete', 'Members\Controllers\MemberController@deletePhoto', ['auth', 'csrf'], 'member.edit'],
['POST', '/members/{id}/status', 'Members\Controllers\MemberController@changeStatus', ['auth', 'csrf'], 'member.change_status'], ['POST', '/members/{id}/status', 'Members\Controllers\MemberController@changeStatus', ['auth', 'csrf'], 'member.change_status'],
['POST', '/members/{id}/pay-form-fee', 'Members\Controllers\MemberController@payFormFee', ['auth', 'csrf'], 'member.pay_form_fee'], ['POST', '/members/{id}/pay-form-fee', 'Members\Controllers\MemberController@payFormFee', ['auth', 'csrf'], 'member.pay_form_fee'],
['POST', '/members/{id}/pay-membership', 'Members\Controllers\MemberController@payMembership',['auth', 'csrf'], 'member.pay_membership'], ['POST', '/members/{id}/pay-membership', 'Members\Controllers\MemberController@payMembership',['auth', 'csrf'], 'member.pay_membership'],
......
This diff is collapsed.
...@@ -403,4 +403,54 @@ class SpouseController extends Controller ...@@ -403,4 +403,54 @@ class SpouseController extends Controller
$result = SpouseFeeCalculator::calculate($memberId, $spouseData); $result = SpouseFeeCalculator::calculate($memberId, $spouseData);
return $this->json($result); return $this->json($result);
} }
public function uploadPhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $id, (int) $memberId]);
if (!$spouse) {
return $this->redirect('/members/' . $memberId)->withError('التابع غير موجود');
}
$photoFile = $_FILES['photo'] ?? [];
if (!PhotoUploadService::isUploaded($photoFile)) {
return $this->redirect('/members/' . $memberId)->withError('لم يتم رفع صورة');
}
$validation = PhotoUploadService::validate($photoFile);
if (!$validation['valid']) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $validation['errors']));
return $this->redirect('/members/' . $memberId);
}
$result = PhotoUploadService::upload($photoFile, 'spouses', (int) $id);
if ($result) {
$db->update('spouses', ['photo_path' => $result['path']], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم رفع صورة الزوج/ة بنجاح');
}
return $this->redirect('/members/' . $memberId)->withError('حدث خطأ أثناء حفظ الصورة');
}
public function deletePhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $id, (int) $memberId]);
if (!$spouse) {
return $this->redirect('/members/' . $memberId)->withError('التابع غير موجود');
}
if (empty($spouse['photo_path'])) {
return $this->redirect('/members/' . $memberId)->withError('لا توجد صورة لحذفها');
}
$fullPath = App::getInstance()->basePath() . '/public/' . $spouse['photo_path'];
if (is_file($fullPath)) {
@unlink($fullPath);
}
$db->update('spouses', ['photo_path' => null], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم حذف صورة الزوج/ة');
}
} }
\ No newline at end of file
...@@ -8,5 +8,7 @@ return [ ...@@ -8,5 +8,7 @@ return [
['GET', '/members/{memberId}/spouses/{id}/edit', 'Spouses\Controllers\SpouseController@edit', ['auth'], 'spouse.edit'], ['GET', '/members/{memberId}/spouses/{id}/edit', 'Spouses\Controllers\SpouseController@edit', ['auth'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@update', ['auth', 'csrf'], 'spouse.edit'], ['POST', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@update', ['auth', 'csrf'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}/archive', 'Spouses\Controllers\SpouseController@archive', ['auth', 'csrf'], 'spouse.remove'], ['POST', '/members/{memberId}/spouses/{id}/archive', 'Spouses\Controllers\SpouseController@archive', ['auth', 'csrf'], 'spouse.remove'],
['POST', '/members/{memberId}/spouses/{id}/photo', 'Spouses\Controllers\SpouseController@uploadPhoto', ['auth', 'csrf'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}/photo/delete', 'Spouses\Controllers\SpouseController@deletePhoto', ['auth', 'csrf'], 'spouse.edit'],
['POST', '/api/spouses/calculate-fee', 'Spouses\Controllers\SpouseController@calculateFee', ['auth'], 'spouse.add'], ['POST', '/api/spouses/calculate-fee', 'Spouses\Controllers\SpouseController@calculateFee', ['auth'], 'spouse.add'],
]; ];
\ No newline at end of file
...@@ -296,4 +296,54 @@ class TemporaryController extends Controller ...@@ -296,4 +296,54 @@ class TemporaryController extends Controller
return $this->redirect("/members/{$memberId}")->withSuccess('تم إزالة العضو المؤقت'); return $this->redirect("/members/{$memberId}")->withSuccess('تم إزالة العضو المؤقت');
} }
public function uploadPhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$temp = $db->selectOne("SELECT * FROM temporary_members WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $id, (int) $memberId]);
if (!$temp) {
return $this->redirect('/members/' . $memberId)->withError('العضو المؤقت غير موجود');
}
$photoFile = $_FILES['photo'] ?? [];
if (!PhotoUploadService::isUploaded($photoFile)) {
return $this->redirect('/members/' . $memberId)->withError('لم يتم رفع صورة');
}
$validation = PhotoUploadService::validate($photoFile);
if (!$validation['valid']) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $validation['errors']));
return $this->redirect('/members/' . $memberId);
}
$result = PhotoUploadService::upload($photoFile, 'temporary', (int) $id);
if ($result) {
$db->update('temporary_members', ['photo_path' => $result['path']], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم رفع صورة العضو المؤقت بنجاح');
}
return $this->redirect('/members/' . $memberId)->withError('حدث خطأ أثناء حفظ الصورة');
}
public function deletePhoto(Request $request, string $memberId, string $id): Response
{
$db = App::getInstance()->db();
$temp = $db->selectOne("SELECT * FROM temporary_members WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $id, (int) $memberId]);
if (!$temp) {
return $this->redirect('/members/' . $memberId)->withError('العضو المؤقت غير موجود');
}
if (empty($temp['photo_path'])) {
return $this->redirect('/members/' . $memberId)->withError('لا توجد صورة لحذفها');
}
$fullPath = App::getInstance()->basePath() . '/public/' . $temp['photo_path'];
if (is_file($fullPath)) {
@unlink($fullPath);
}
$db->update('temporary_members', ['photo_path' => null], 'id = ?', [(int) $id]);
return $this->redirect('/members/' . $memberId)->withSuccess('تم حذف صورة العضو المؤقت');
}
} }
\ No newline at end of file
...@@ -7,4 +7,6 @@ return [ ...@@ -7,4 +7,6 @@ return [
['POST', '/members/{memberId}/temporary', 'Temporary\Controllers\TemporaryController@store', ['auth', 'csrf'], 'temp.add'], ['POST', '/members/{memberId}/temporary', 'Temporary\Controllers\TemporaryController@store', ['auth', 'csrf'], 'temp.add'],
['GET', '/members/{memberId}/temporary/{id}', 'Temporary\Controllers\TemporaryController@show', ['auth'], 'temp.view'], ['GET', '/members/{memberId}/temporary/{id}', 'Temporary\Controllers\TemporaryController@show', ['auth'], 'temp.view'],
['POST', '/members/{memberId}/temporary/{id}/archive', 'Temporary\Controllers\TemporaryController@archive', ['auth', 'csrf'], 'temp.remove'], ['POST', '/members/{memberId}/temporary/{id}/archive', 'Temporary\Controllers\TemporaryController@archive', ['auth', 'csrf'], 'temp.remove'],
['POST', '/members/{memberId}/temporary/{id}/photo', 'Temporary\Controllers\TemporaryController@uploadPhoto', ['auth', 'csrf'], 'temp.add'],
['POST', '/members/{memberId}/temporary/{id}/photo/delete', 'Temporary\Controllers\TemporaryController@deletePhoto', ['auth', 'csrf'], 'temp.add'],
]; ];
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment