430 lines
15 KiB
PHP
430 lines
15 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../app/lib/auth.php';
|
|
require_once __DIR__ . '/../app/lib/db.php';
|
|
require_once __DIR__ . '/../app/lib/helpers.php';
|
|
|
|
check_auth();
|
|
|
|
$pdo = db();
|
|
$uid = user_id();
|
|
$error = '';
|
|
$msg = '';
|
|
|
|
$ym = $_GET['ym'] ?? date('Y-m');
|
|
$accountId = (int)($_GET['account_id'] ?? 0);
|
|
$q = trim($_GET['q'] ?? '');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
try {
|
|
$mode = $_POST['mode'] ?? '';
|
|
|
|
require_once __DIR__ . '/../app/lib/installment_service.php';
|
|
|
|
if ($mode === 'mark_billed') {
|
|
$scheduleId = (int)($_POST['schedule_id'] ?? 0);
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT s.installment_id
|
|
FROM installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
WHERE s.id = ?
|
|
AND i.user_id = ?
|
|
");
|
|
$stmt->execute([$scheduleId, $uid]);
|
|
$row = $stmt->fetch();
|
|
|
|
if (!$row) {
|
|
throw new RuntimeException('회차를 찾을 수 없습니다.');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
SET s.is_billed = 1,
|
|
s.billed_at = NOW()
|
|
WHERE s.id = ?
|
|
AND i.user_id = ?
|
|
");
|
|
$stmt->execute([$scheduleId, $uid]);
|
|
|
|
recalculate_installment_status((int)$row['installment_id']);
|
|
$msg = '청구완료 처리되었습니다.';
|
|
}
|
|
|
|
if ($mode === 'mark_unbilled') {
|
|
$scheduleId = (int)($_POST['schedule_id'] ?? 0);
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT s.installment_id
|
|
FROM installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
WHERE s.id = ?
|
|
AND i.user_id = ?
|
|
");
|
|
$stmt->execute([$scheduleId, $uid]);
|
|
$row = $stmt->fetch();
|
|
|
|
if (!$row) {
|
|
throw new RuntimeException('회차를 찾을 수 없습니다.');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
UPDATE installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
SET s.is_billed = 0,
|
|
s.billed_at = NULL
|
|
WHERE s.id = ?
|
|
AND i.user_id = ?
|
|
");
|
|
$stmt->execute([$scheduleId, $uid]);
|
|
|
|
recalculate_installment_status((int)$row['installment_id']);
|
|
$msg = '청구완료 취소되었습니다.';
|
|
}
|
|
|
|
if ($mode === 'mark_month_all_billed' || $mode === 'mark_month_all_unbilled') {
|
|
$targetYm = trim($_POST['target_ym'] ?? '');
|
|
|
|
if ($targetYm === '') {
|
|
throw new RuntimeException('대상 월이 없습니다.');
|
|
}
|
|
|
|
$wantBilled = ($mode === 'mark_month_all_billed');
|
|
$fromState = $wantBilled ? 0 : 1;
|
|
|
|
$params = [$uid, $targetYm];
|
|
$sql = "
|
|
SELECT s.id, s.installment_id
|
|
FROM installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
JOIN transactions t ON t.id = i.transaction_id
|
|
WHERE i.user_id = ?
|
|
AND s.bill_year_month = ?
|
|
AND s.is_billed = ?
|
|
";
|
|
$params[] = $fromState;
|
|
|
|
if ($accountId > 0) {
|
|
$sql .= " AND i.account_id = ? ";
|
|
$params[] = $accountId;
|
|
}
|
|
|
|
if ($q !== '') {
|
|
$sql .= " AND (t.merchant_name LIKE ? OR t.description LIKE ?) ";
|
|
$like = '%' . $q . '%';
|
|
$params[] = $like;
|
|
$params[] = $like;
|
|
}
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$rows = $stmt->fetchAll();
|
|
|
|
if ($rows) {
|
|
$scheduleIds = array_column($rows, 'id');
|
|
$installmentIds = array_values(array_unique(array_column($rows, 'installment_id')));
|
|
|
|
$placeholders = implode(',', array_fill(0, count($scheduleIds), '?'));
|
|
|
|
if ($wantBilled) {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE installment_schedules
|
|
SET is_billed = 1,
|
|
billed_at = NOW()
|
|
WHERE id IN ($placeholders)
|
|
");
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE installment_schedules
|
|
SET is_billed = 0,
|
|
billed_at = NULL
|
|
WHERE id IN ($placeholders)
|
|
");
|
|
}
|
|
|
|
$stmt->execute($scheduleIds);
|
|
|
|
foreach ($installmentIds as $iid) {
|
|
recalculate_installment_status((int)$iid);
|
|
}
|
|
}
|
|
|
|
$msg = $wantBilled
|
|
? $targetYm . ' 조회건 전체 청구완료 처리되었습니다.'
|
|
: $targetYm . ' 조회건 전체 취소되었습니다.';
|
|
}
|
|
|
|
} catch (Throwable $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
$params = [$uid, $ym];
|
|
$where = [
|
|
"i.user_id = ?",
|
|
"s.bill_year_month = ?"
|
|
];
|
|
|
|
if ($accountId > 0) {
|
|
$where[] = "i.account_id = ?";
|
|
$params[] = $accountId;
|
|
}
|
|
|
|
if ($q !== '') {
|
|
$where[] = "(t.merchant_name LIKE ? OR t.description LIKE ? OR a.account_name LIKE ?)";
|
|
$like = '%' . $q . '%';
|
|
$params[] = $like;
|
|
$params[] = $like;
|
|
$params[] = $like;
|
|
}
|
|
|
|
$sql = "
|
|
SELECT
|
|
s.id AS schedule_id,
|
|
s.installment_id,
|
|
s.cycle_no,
|
|
s.bill_year_month,
|
|
s.principal_amount,
|
|
s.interest_amount,
|
|
s.total_amount,
|
|
s.is_billed,
|
|
s.billed_at,
|
|
|
|
i.installment_months,
|
|
i.annual_interest_rate,
|
|
|
|
a.account_name,
|
|
a.institution_name,
|
|
|
|
t.merchant_name,
|
|
t.description
|
|
|
|
FROM installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
JOIN accounts a ON a.id = i.account_id
|
|
JOIN transactions t ON t.id = i.transaction_id
|
|
WHERE " . implode(' AND ', $where) . "
|
|
ORDER BY s.is_billed ASC, a.account_name ASC, s.cycle_no ASC, s.id ASC
|
|
";
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$list = $stmt->fetchAll();
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT id, account_name, institution_name
|
|
FROM accounts
|
|
WHERE user_id = ?
|
|
AND is_active = 1
|
|
AND account_type = 'card'
|
|
ORDER BY id ASC
|
|
");
|
|
$stmt->execute([$uid]);
|
|
$cardAccounts = $stmt->fetchAll();
|
|
|
|
$summaryStmt = $pdo->prepare("
|
|
SELECT
|
|
COALESCE(SUM(CASE WHEN s.is_billed=0 THEN s.principal_amount ELSE 0 END),0) AS unbilled_principal,
|
|
COALESCE(SUM(CASE WHEN s.is_billed=0 THEN s.interest_amount ELSE 0 END),0) AS unbilled_interest,
|
|
COALESCE(SUM(CASE WHEN s.is_billed=0 THEN s.total_amount ELSE 0 END),0) AS unbilled_total,
|
|
COALESCE(SUM(CASE WHEN s.is_billed=1 THEN s.total_amount ELSE 0 END),0) AS billed_total
|
|
FROM installment_schedules s
|
|
JOIN installments i ON i.id = s.installment_id
|
|
JOIN transactions t ON t.id = i.transaction_id
|
|
JOIN accounts a ON a.id = i.account_id
|
|
WHERE " . implode(' AND ', $where));
|
|
|
|
$summaryStmt->execute($params);
|
|
$summary = $summaryStmt->fetch();
|
|
|
|
require __DIR__ . '/../app/views/header.php';
|
|
?>
|
|
|
|
<div class="page-head">
|
|
<h2>할부 청구 관리</h2>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<a href="/installments.php" class="btn btn-outline-secondary">할부 목록</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger"><?= h($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($msg): ?>
|
|
<div class="alert alert-success"><?= h($msg) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card finance-card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3">
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">청구월</label>
|
|
<input type="month" name="ym" class="form-control" value="<?= h($ym) ?>">
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">카드</label>
|
|
<select name="account_id" class="form-select">
|
|
<option value="0">전체</option>
|
|
<?php foreach ($cardAccounts as $acc): ?>
|
|
<option value="<?= $acc['id'] ?>" <?= $accountId === (int)$acc['id'] ? 'selected' : '' ?>>
|
|
<?= h($acc['institution_name']) ?> / <?= h($acc['account_name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-4">
|
|
<label class="form-label">검색</label>
|
|
<input type="text" name="q" class="form-control" value="<?= h($q) ?>" placeholder="사용처, 메모, 카드명">
|
|
</div>
|
|
|
|
<div class="col-12 col-md-2 d-flex align-items-end gap-2">
|
|
<button class="btn btn-primary w-100">조회</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-4">
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card finance-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">미청구 원금</div>
|
|
<div class="stat-value"><?= won($summary['unbilled_principal']) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card finance-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">미청구 이자</div>
|
|
<div class="stat-value text-danger"><?= won($summary['unbilled_interest']) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card finance-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">미청구 합계</div>
|
|
<div class="stat-value"><?= won($summary['unbilled_total']) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card finance-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">청구완료 합계</div>
|
|
<div class="stat-value text-primary"><?= won($summary['billed_total']) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-4">
|
|
<form method="post">
|
|
<input type="hidden" name="mode" value="mark_month_all_billed">
|
|
<input type="hidden" name="target_ym" value="<?= h($ym) ?>">
|
|
<button class="btn btn-primary" onclick="return confirm('조회 조건 전체를 청구완료 처리하시겠습니까?');">
|
|
전체 청구완료
|
|
</button>
|
|
</form>
|
|
|
|
<form method="post">
|
|
<input type="hidden" name="mode" value="mark_month_all_unbilled">
|
|
<input type="hidden" name="target_ym" value="<?= h($ym) ?>">
|
|
<button class="btn btn-outline-danger" onclick="return confirm('조회 조건 전체를 취소하시겠습니까?');">
|
|
전체 취소
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card finance-card">
|
|
<div class="card-body mobile-scroll">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>카드</th>
|
|
<th>사용처</th>
|
|
<th>회차</th>
|
|
<th>청구월</th>
|
|
<th class="text-end">원금</th>
|
|
<th class="text-end">이자</th>
|
|
<th class="text-end">합계</th>
|
|
<th>상태</th>
|
|
<th>관리</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
<?php foreach ($list as $row): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold"><?= h($row['account_name']) ?></div>
|
|
<div class="small text-secondary"><?= h($row['institution_name']) ?></div>
|
|
</td>
|
|
|
|
<td>
|
|
<div class="fw-bold"><?= h($row['merchant_name'] ?: '-') ?></div>
|
|
<div class="small text-secondary"><?= h($row['description'] ?: '-') ?></div>
|
|
<div class="small text-secondary">연이자율 <?= numf($row['annual_interest_rate'], 2) ?>%</div>
|
|
</td>
|
|
|
|
<td><?= intvalf($row['cycle_no']) ?> / <?= intvalf($row['installment_months']) ?></td>
|
|
<td><?= h($row['bill_year_month']) ?></td>
|
|
|
|
<td class="text-end"><?= won($row['principal_amount']) ?></td>
|
|
<td class="text-end text-danger"><?= won($row['interest_amount']) ?></td>
|
|
<td class="text-end fw-bold"><?= won($row['total_amount']) ?></td>
|
|
|
|
<td>
|
|
<?php if ((int)$row['is_billed'] === 1): ?>
|
|
<span class="badge text-bg-success">청구완료</span>
|
|
<?php if (!empty($row['billed_at'])): ?>
|
|
<div class="small text-secondary mt-1">
|
|
<?= h(date('Y-m-d H:i', strtotime($row['billed_at']))) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<span class="badge text-bg-secondary">미청구</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
|
|
<td class="text-nowrap">
|
|
<?php if ((int)$row['is_billed'] === 0): ?>
|
|
<form method="post" class="d-inline">
|
|
<input type="hidden" name="mode" value="mark_billed">
|
|
<input type="hidden" name="schedule_id" value="<?= $row['schedule_id'] ?>">
|
|
<button class="btn btn-sm btn-primary">완료</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<form method="post" class="d-inline">
|
|
<input type="hidden" name="mode" value="mark_unbilled">
|
|
<input type="hidden" name="schedule_id" value="<?= $row['schedule_id'] ?>">
|
|
<button class="btn btn-sm btn-outline-danger">취소</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if (!$list): ?>
|
|
<tr>
|
|
<td colspan="9" class="text-center text-secondary py-5">조회 결과가 없습니다.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php require __DIR__ . '/../app/views/footer.php'; ?>
|