- Loan.php: User import 누락 → App\Models\Members\User 추가 - TodayIssue.php: App\Models\Users\User → App\Models\Members\User 수정 - Tenants 네임스페이스에서 User::class가 App\Models\Tenants\User로 잘못 해석되는 문제 해결
292 lines
8.1 KiB
PHP
292 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Tenants;
|
|
|
|
use App\Models\Members\User;
|
|
use App\Traits\BelongsToTenant;
|
|
use App\Traits\ModelTrait;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
/**
|
|
* 오늘의 이슈 모델
|
|
*
|
|
* CEO 대시보드용 실시간 이슈 저장
|
|
*/
|
|
class TodayIssue extends Model
|
|
{
|
|
use BelongsToTenant, HasFactory, ModelTrait;
|
|
|
|
protected $table = 'today_issues';
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'source_type',
|
|
'source_id',
|
|
'target_user_id',
|
|
'badge',
|
|
'notification_type',
|
|
'content',
|
|
'path',
|
|
'needs_approval',
|
|
'is_read',
|
|
'read_by',
|
|
'read_at',
|
|
'expires_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'needs_approval' => 'boolean',
|
|
'is_read' => 'boolean',
|
|
'read_at' => 'datetime',
|
|
'expires_at' => 'datetime',
|
|
];
|
|
|
|
// 소스 타입 상수
|
|
public const SOURCE_ORDER = 'order';
|
|
|
|
public const SOURCE_BAD_DEBT = 'bad_debt';
|
|
|
|
public const SOURCE_STOCK = 'stock';
|
|
|
|
public const SOURCE_EXPENSE = 'expense';
|
|
|
|
public const SOURCE_TAX = 'tax';
|
|
|
|
public const SOURCE_APPROVAL = 'approval';
|
|
|
|
public const SOURCE_CLIENT = 'client';
|
|
|
|
public const SOURCE_DEPOSIT = 'deposit';
|
|
|
|
public const SOURCE_WITHDRAWAL = 'withdrawal';
|
|
|
|
// 뱃지 타입 상수 (최대 4자, 띄어쓰기 없음)
|
|
public const BADGE_ORDER_REGISTER = '수주등록';
|
|
|
|
public const BADGE_COLLECTION_ISSUE = '추심이슈';
|
|
|
|
public const BADGE_SAFETY_STOCK = '안전재고';
|
|
|
|
public const BADGE_EXPENSE_PENDING = '지출승인';
|
|
|
|
public const BADGE_TAX_REPORT = '세금신고';
|
|
|
|
public const BADGE_APPROVAL_REQUEST = '결재요청';
|
|
|
|
public const BADGE_NEW_CLIENT = '신규업체';
|
|
|
|
public const BADGE_DEPOSIT = '입금';
|
|
|
|
public const BADGE_WITHDRAWAL = '출금';
|
|
|
|
// 기안 상태 변경 알림 뱃지
|
|
public const BADGE_DRAFT_APPROVED = '기안승인';
|
|
|
|
public const BADGE_DRAFT_REJECTED = '기안반려';
|
|
|
|
public const BADGE_DRAFT_COMPLETED = '기안완료';
|
|
|
|
// source_type → badge 매핑
|
|
public const SOURCE_TO_BADGE = [
|
|
self::SOURCE_ORDER => self::BADGE_ORDER_REGISTER,
|
|
self::SOURCE_BAD_DEBT => self::BADGE_COLLECTION_ISSUE,
|
|
self::SOURCE_STOCK => self::BADGE_SAFETY_STOCK,
|
|
self::SOURCE_EXPENSE => self::BADGE_EXPENSE_PENDING,
|
|
self::SOURCE_TAX => self::BADGE_TAX_REPORT,
|
|
self::SOURCE_APPROVAL => self::BADGE_APPROVAL_REQUEST,
|
|
self::SOURCE_CLIENT => self::BADGE_NEW_CLIENT,
|
|
self::SOURCE_DEPOSIT => self::BADGE_DEPOSIT,
|
|
self::SOURCE_WITHDRAWAL => self::BADGE_WITHDRAWAL,
|
|
];
|
|
|
|
// 뱃지 → notification_type 매핑
|
|
public const BADGE_TO_NOTIFICATION_TYPE = [
|
|
self::BADGE_ORDER_REGISTER => 'sales_order',
|
|
self::BADGE_NEW_CLIENT => 'new_vendor',
|
|
self::BADGE_APPROVAL_REQUEST => 'approval_request',
|
|
self::BADGE_DRAFT_APPROVED => 'approval_request',
|
|
self::BADGE_DRAFT_REJECTED => 'approval_request',
|
|
self::BADGE_DRAFT_COMPLETED => 'approval_request',
|
|
self::BADGE_COLLECTION_ISSUE => 'bad_debt',
|
|
self::BADGE_SAFETY_STOCK => 'safety_stock',
|
|
self::BADGE_EXPENSE_PENDING => 'expected_expense',
|
|
self::BADGE_TAX_REPORT => 'vat_report',
|
|
self::BADGE_DEPOSIT => 'deposit',
|
|
self::BADGE_WITHDRAWAL => 'withdrawal',
|
|
];
|
|
|
|
// 중요 알림 (긴급 푸시) - 수주등록, 신규거래처, 결재요청
|
|
// 입금, 출금, 카드 등은 일반 푸시(push_default)로 발송
|
|
public const IMPORTANT_NOTIFICATIONS = [
|
|
'sales_order',
|
|
'new_vendor',
|
|
'approval_request',
|
|
];
|
|
|
|
/**
|
|
* 확인한 사용자
|
|
*/
|
|
public function reader(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'read_by');
|
|
}
|
|
|
|
/**
|
|
* 대상 사용자 (특정 사용자에게만 발송할 경우)
|
|
*/
|
|
public function targetUser(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'target_user_id');
|
|
}
|
|
|
|
/**
|
|
* 읽지 않은 이슈 스코프
|
|
*/
|
|
public function scopeUnread($query)
|
|
{
|
|
return $query->where('is_read', false);
|
|
}
|
|
|
|
/**
|
|
* 만료되지 않은 이슈 스코프
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where(function ($q) {
|
|
$q->whereNull('expires_at')
|
|
->orWhere('expires_at', '>', now());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 오늘 날짜 이슈 스코프
|
|
*/
|
|
public function scopeToday($query)
|
|
{
|
|
return $query->whereDate('created_at', today());
|
|
}
|
|
|
|
/**
|
|
* 뱃지별 필터 스코프
|
|
*/
|
|
public function scopeByBadge($query, string $badge)
|
|
{
|
|
return $query->where('badge', $badge);
|
|
}
|
|
|
|
/**
|
|
* 소스별 필터 스코프
|
|
*/
|
|
public function scopeBySource($query, string $sourceType, ?int $sourceId = null)
|
|
{
|
|
$query->where('source_type', $sourceType);
|
|
|
|
if ($sourceId !== null) {
|
|
$query->where('source_id', $sourceId);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* 특정 사용자 대상 이슈 스코프
|
|
* target_user_id가 null이거나 지정된 사용자인 경우
|
|
*/
|
|
public function scopeForUser($query, int $userId)
|
|
{
|
|
return $query->where(function ($q) use ($userId) {
|
|
$q->whereNull('target_user_id')
|
|
->orWhere('target_user_id', $userId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 특정 사용자만 대상인 이슈 스코프
|
|
*/
|
|
public function scopeTargetedTo($query, int $userId)
|
|
{
|
|
return $query->where('target_user_id', $userId);
|
|
}
|
|
|
|
/**
|
|
* 이슈 확인 처리
|
|
*/
|
|
public function markAsRead(int $userId): bool
|
|
{
|
|
return $this->update([
|
|
'is_read' => true,
|
|
'read_by' => $userId,
|
|
'read_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 이슈 생성 헬퍼 (정적 메서드)
|
|
*
|
|
* @param int $tenantId 테넌트 ID
|
|
* @param string $sourceType 소스 타입 (order, approval, etc.)
|
|
* @param int|null $sourceId 소스 ID
|
|
* @param string $badge 뱃지 타입
|
|
* @param string $content 알림 내용
|
|
* @param string|null $path 이동 경로
|
|
* @param bool $needsApproval 승인 필요 여부
|
|
* @param \DateTime|null $expiresAt 만료 시간
|
|
* @param int|null $targetUserId 특정 대상 사용자 ID (null이면 테넌트 전체)
|
|
*/
|
|
public static function createIssue(
|
|
int $tenantId,
|
|
string $sourceType,
|
|
?int $sourceId,
|
|
string $badge,
|
|
string $content,
|
|
?string $path = null,
|
|
bool $needsApproval = false,
|
|
?\DateTime $expiresAt = null,
|
|
?int $targetUserId = null
|
|
): self {
|
|
// badge에서 notification_type 자동 매핑
|
|
$notificationType = self::BADGE_TO_NOTIFICATION_TYPE[$badge] ?? null;
|
|
|
|
return self::updateOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'source_type' => $sourceType,
|
|
'source_id' => $sourceId,
|
|
'target_user_id' => $targetUserId,
|
|
],
|
|
[
|
|
'badge' => $badge,
|
|
'notification_type' => $notificationType,
|
|
'content' => $content,
|
|
'path' => $path,
|
|
'needs_approval' => $needsApproval,
|
|
'expires_at' => $expiresAt,
|
|
'is_read' => false,
|
|
'read_by' => null,
|
|
'read_at' => null,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 중요 알림 여부 확인
|
|
*/
|
|
public function isImportantNotification(): bool
|
|
{
|
|
return in_array($this->notification_type, self::IMPORTANT_NOTIFICATIONS, true);
|
|
}
|
|
|
|
/**
|
|
* 소스 기준 이슈 삭제 헬퍼 (정적 메서드)
|
|
*/
|
|
public static function removeBySource(int $tenantId, string $sourceType, int $sourceId): bool
|
|
{
|
|
return self::where('tenant_id', $tenantId)
|
|
->where('source_type', $sourceType)
|
|
->where('source_id', $sourceId)
|
|
->delete() > 0;
|
|
}
|
|
}
|