Files
financial/public/installment_billing.php
2026-06-07 00:33:58 +09:00

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'; ?>