Compare commits
37 Commits
fb24ffbf03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ae7fedef | ||
|
|
8d7cf86d4d | ||
|
|
81417b2a1c | ||
|
|
c0463a8f5b | ||
|
|
c4f1a1ca81 | ||
|
|
c523552d2f | ||
|
|
1e16dc5f18 | ||
|
|
313c4d6c60 | ||
|
|
0cbe46c93a | ||
|
|
55a118f8c9 | ||
|
|
056cac794b | ||
|
|
df2aaa38a3 | ||
|
|
d06dfed0e2 | ||
|
|
2161ebf1e8 | ||
|
|
42546af4f3 | ||
|
|
bd39a45dd2 | ||
|
|
cd2c2ac50d | ||
|
|
2bbd67e37d | ||
|
|
a2e14c73df | ||
|
|
c2161bdb91 | ||
|
|
49d32b0854 | ||
|
|
80b0e825b2 | ||
|
|
53b4a5e97d | ||
|
|
335112e044 | ||
|
|
9299e29ea6 | ||
|
|
5a17df917d | ||
|
|
8aed8d16b6 | ||
|
|
64aa9cf716 | ||
|
|
f63ccc2a38 | ||
|
|
a6fd3d720f | ||
|
|
77f6d2a475 | ||
|
|
85f559a607 | ||
|
|
0c2f131fff | ||
|
|
e96696f6be | ||
|
|
704e206476 | ||
|
|
6b19f08d6b | ||
|
|
146c557c25 |
17
.drone.yml
17
.drone.yml
@@ -24,23 +24,6 @@ steps:
|
|||||||
- echo "$GITEA_TOKEN" | docker login 192.168.1.9:3000 -u "$GITEA_USERNAME" --password-stdin
|
- echo "$GITEA_TOKEN" | docker login 192.168.1.9:3000 -u "$GITEA_USERNAME" --password-stdin
|
||||||
- docker buildx build --builder default --tag 192.168.1.9:3000/tai/aberwyn/aberwyn:latest --tag 192.168.1.9:3000/tai/aberwyn/aberwyn:${DRONE_COMMIT_SHA:0:7} --cache-from=type=registry,ref=192.168.1.9:3000/tai/aberwyn/aberwyn:buildcache --push -f Aberwyn/Dockerfile .
|
- docker buildx build --builder default --tag 192.168.1.9:3000/tai/aberwyn/aberwyn:latest --tag 192.168.1.9:3000/tai/aberwyn/aberwyn:${DRONE_COMMIT_SHA:0:7} --cache-from=type=registry,ref=192.168.1.9:3000/tai/aberwyn/aberwyn:buildcache --push -f Aberwyn/Dockerfile .
|
||||||
|
|
||||||
- name: restart-unraid-container
|
|
||||||
image: appleboy/drone-ssh
|
|
||||||
settings:
|
|
||||||
host: 192.168.1.108
|
|
||||||
port: 22
|
|
||||||
username:
|
|
||||||
from_secret: unraid_ssh_user
|
|
||||||
key:
|
|
||||||
from_secret: unraid_ssh_private_key
|
|
||||||
script:
|
|
||||||
- docker pull 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
|
|
||||||
- docker stop aberwyn || true
|
|
||||||
- docker rm aberwyn || true
|
|
||||||
- docker volume create aberwyn_config || true
|
|
||||||
- docker volume create aberwyn_keys || true
|
|
||||||
- docker run -d --name=aberwyn --net=br0 -e TZ=Europe/Berlin -p 80:80 -v aberwyn_config:/app/infrastructure -v aberwyn_keys:/root/.aspnet/DataProtection-Keys 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
|
|
||||||
|
|
||||||
- name: notify-result
|
- name: notify-result
|
||||||
image: alpine
|
image: alpine
|
||||||
when:
|
when:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -365,3 +365,4 @@ FodyWeavers.xsd
|
|||||||
# Setupfil för Aberwyn
|
# Setupfil för Aberwyn
|
||||||
infrastructure/setup.json
|
infrastructure/setup.json
|
||||||
Aberwyn/Infrastructure/setup.json
|
Aberwyn/Infrastructure/setup.json
|
||||||
|
/Aberwyn/Data/infrastructure/setup2.json
|
||||||
|
|||||||
@@ -23,21 +23,6 @@ steps:
|
|||||||
- latest
|
- latest
|
||||||
insecure: true
|
insecure: true
|
||||||
|
|
||||||
- name: restart-unraid-container
|
|
||||||
image: appleboy/drone-ssh
|
|
||||||
settings:
|
|
||||||
host: 192.168.1.108
|
|
||||||
port: 22
|
|
||||||
username:
|
|
||||||
from_secret: unraid_ssh_user
|
|
||||||
key:
|
|
||||||
from_secret: unraid_ssh_private_key
|
|
||||||
script:
|
|
||||||
- docker pull 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
|
|
||||||
- docker stop Aberwyn || true
|
|
||||||
- docker rm Aberwyn || true
|
|
||||||
- docker run -d --name='Aberwyn' --net='br0' -e TZ='Europe/Berlin' -p 80:80 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
|
|
||||||
|
|
||||||
- name: notify-result
|
- name: notify-result
|
||||||
image: alpine
|
image: alpine
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -9,11 +9,16 @@
|
|||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Remove="Views\Rss\_RssListPartial.cshtml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
|
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
|
||||||
<PackageReference Include="BencodeNET" Version="5.0.0" />
|
<PackageReference Include="BencodeNET" Version="5.0.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
||||||
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.3.1" />
|
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.3.1" />
|
||||||
|
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||||
|
|
||||||
<!-- Entity Framework Core 6 -->
|
<!-- Entity Framework Core 6 -->
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
|
||||||
@@ -39,7 +44,10 @@
|
|||||||
<!-- Övrigt -->
|
<!-- Övrigt -->
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="10.0.2" />
|
||||||
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="WebPush" Version="1.0.12" />
|
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace Aberwyn.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
var period = await _context.BudgetPeriods
|
var period = await _context.BudgetPeriods
|
||||||
.Include(p => p.Categories)
|
.Include(p => p.Categories)
|
||||||
.ThenInclude(c => c.Items)
|
.ThenInclude(c => c.Items)
|
||||||
@@ -78,10 +79,13 @@ namespace Aberwyn.Controllers
|
|||||||
[HttpGet("byname/{name}")]
|
[HttpGet("byname/{name}")]
|
||||||
public async Task<IActionResult> GetBudgetByName(string name)
|
public async Task<IActionResult> GetBudgetByName(string name)
|
||||||
{
|
{
|
||||||
var period = await _context.BudgetPeriods
|
var period = _context.BudgetPeriods
|
||||||
.Include(p => p.Categories)
|
.Include(p => p.Categories)
|
||||||
.ThenInclude(c => c.Items)
|
.ThenInclude(c => c.Items)
|
||||||
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower());
|
.AsEnumerable() // hämta från db och gör resten i minnet
|
||||||
|
.FirstOrDefault(p => p.Name != null &&
|
||||||
|
p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|
||||||
if (period == null)
|
if (period == null)
|
||||||
{
|
{
|
||||||
@@ -363,6 +367,73 @@ namespace Aberwyn.Controllers
|
|||||||
return Ok(new { id = newItem.Id });
|
return Ok(new { id = newItem.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<IActionResult> GetAllBudgets()
|
||||||
|
{
|
||||||
|
var periods = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.OrderByDescending(p => p.Year)
|
||||||
|
.ThenByDescending(p => p.Month)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var result = periods.Select(p => new
|
||||||
|
{
|
||||||
|
id = p.Id,
|
||||||
|
name = p.Name,
|
||||||
|
year = p.Year,
|
||||||
|
month = p.Month,
|
||||||
|
categories = p.Categories
|
||||||
|
.OrderBy(c => c.Order)
|
||||||
|
.Select(c => new
|
||||||
|
{
|
||||||
|
id = c.Id,
|
||||||
|
name = c.Name,
|
||||||
|
color = c.Color,
|
||||||
|
total = c.Items.Sum(i => i.Amount),
|
||||||
|
items = c.Items
|
||||||
|
.OrderBy(i => i.Order)
|
||||||
|
.Select(i => new
|
||||||
|
{
|
||||||
|
id = i.Id,
|
||||||
|
name = i.Name,
|
||||||
|
amount = i.Amount,
|
||||||
|
isExpense = i.IsExpense,
|
||||||
|
includeInSummary = i.IncludeInSummary
|
||||||
|
}).ToList()
|
||||||
|
}).ToList(),
|
||||||
|
total = p.Categories.Sum(c => c.Items.Sum(i => i.Amount))
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// DELETE: api/budget/byname/{name}
|
||||||
|
[HttpDelete("byname/{name}")]
|
||||||
|
public async Task<IActionResult> DeleteByName(string name)
|
||||||
|
{
|
||||||
|
var period = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower());
|
||||||
|
|
||||||
|
if (period == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
foreach (var category in period.Categories)
|
||||||
|
{
|
||||||
|
_context.BudgetItems.RemoveRange(category.Items);
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.BudgetCategories.RemoveRange(period.Categories);
|
||||||
|
_context.BudgetPeriods.Remove(period);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpDelete("item/{id}")]
|
[HttpDelete("item/{id}")]
|
||||||
@@ -462,7 +533,7 @@ namespace Aberwyn.Controllers
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("copy/{year:int}/{month:int}")]
|
/*[HttpPost("copy/{year:int}/{month:int}")]
|
||||||
public async Task<IActionResult> CopyFromPreviousMonth(int year, int month)
|
public async Task<IActionResult> CopyFromPreviousMonth(int year, int month)
|
||||||
{
|
{
|
||||||
var targetPeriod = await _context.BudgetPeriods
|
var targetPeriod = await _context.BudgetPeriods
|
||||||
@@ -508,9 +579,333 @@ namespace Aberwyn.Controllers
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Gemensam intern metod
|
||||||
|
private async Task<BudgetPeriod?> CopyBudgetAsync(
|
||||||
|
BudgetPeriod targetPeriod,
|
||||||
|
BudgetPeriod sourcePeriod)
|
||||||
|
{
|
||||||
|
if (sourcePeriod == null) return null;
|
||||||
|
|
||||||
|
targetPeriod.Categories = sourcePeriod.Categories.Select(cat => new BudgetCategory
|
||||||
|
{
|
||||||
|
Name = cat.Name,
|
||||||
|
Color = cat.Color,
|
||||||
|
Order = cat.Order,
|
||||||
|
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId,
|
||||||
|
Items = cat.Items.Select(item => new BudgetItem
|
||||||
|
{
|
||||||
|
Name = item.Name,
|
||||||
|
Amount = item.Amount,
|
||||||
|
IsExpense = item.IsExpense,
|
||||||
|
IncludeInSummary = item.IncludeInSummary,
|
||||||
|
Order = item.Order,
|
||||||
|
BudgetItemDefinitionId = item.BudgetItemDefinitionId
|
||||||
|
}).ToList()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return targetPeriod;
|
||||||
|
}
|
||||||
|
[HttpPost("copy/byname/{targetName}")]
|
||||||
|
public async Task<IActionResult> CopyToNamedBudget(
|
||||||
|
string targetName,
|
||||||
|
[FromQuery] string? from,
|
||||||
|
[FromQuery] int? fromYear,
|
||||||
|
[FromQuery] int? fromMonth)
|
||||||
|
{
|
||||||
|
var targetPeriod = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == targetName.ToLower());
|
||||||
|
|
||||||
|
if (targetPeriod == null)
|
||||||
|
{
|
||||||
|
targetPeriod = new BudgetPeriod { Name = targetName };
|
||||||
|
_context.BudgetPeriods.Add(targetPeriod);
|
||||||
|
}
|
||||||
|
else if (targetPeriod.Categories.Any())
|
||||||
|
{
|
||||||
|
return BadRequest("Det finns redan data för denna budget.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hämta källperiod
|
||||||
|
var sourcePeriod = await FindSourcePeriod(from, fromYear, fromMonth);
|
||||||
|
if (sourcePeriod == null)
|
||||||
|
return NotFound("Ingen budget hittades att kopiera från.");
|
||||||
|
|
||||||
|
await CopyBudgetAsync(targetPeriod, sourcePeriod);
|
||||||
|
return Ok(new { id = targetPeriod.Id });
|
||||||
|
}
|
||||||
|
[HttpPost("copy/{year:int}/{month:int}")]
|
||||||
|
public async Task<IActionResult> CopyToYearMonth(
|
||||||
|
int year,
|
||||||
|
int month,
|
||||||
|
[FromQuery] string? from,
|
||||||
|
[FromQuery] int? fromYear,
|
||||||
|
[FromQuery] int? fromMonth)
|
||||||
|
{
|
||||||
|
var targetPeriod = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Year == year && p.Month == month);
|
||||||
|
|
||||||
|
if (targetPeriod == null)
|
||||||
|
{
|
||||||
|
targetPeriod = new BudgetPeriod { Year = year, Month = month };
|
||||||
|
_context.BudgetPeriods.Add(targetPeriod);
|
||||||
|
}
|
||||||
|
else if (targetPeriod.Categories.Any())
|
||||||
|
{
|
||||||
|
return BadRequest("Det finns redan data för denna månad.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hämta källperiod
|
||||||
|
var sourcePeriod = await FindSourcePeriod(from, fromYear, fromMonth, year, month);
|
||||||
|
if (sourcePeriod == null)
|
||||||
|
return NotFound("Ingen data att kopiera från.");
|
||||||
|
|
||||||
|
await CopyBudgetAsync(targetPeriod, sourcePeriod);
|
||||||
|
return Ok(new { id = targetPeriod.Id });
|
||||||
|
}
|
||||||
|
private async Task<BudgetPeriod?> FindSourcePeriod(
|
||||||
|
string? from,
|
||||||
|
int? fromYear,
|
||||||
|
int? fromMonth,
|
||||||
|
int? defaultYear = null,
|
||||||
|
int? defaultMonth = null)
|
||||||
|
{
|
||||||
|
BudgetPeriod? sourcePeriod = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(from))
|
||||||
|
{
|
||||||
|
sourcePeriod = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == from.ToLower());
|
||||||
|
}
|
||||||
|
else if (fromYear.HasValue && fromMonth.HasValue)
|
||||||
|
{
|
||||||
|
sourcePeriod = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Year == fromYear && p.Month == fromMonth);
|
||||||
|
}
|
||||||
|
else if (defaultYear.HasValue && defaultMonth.HasValue)
|
||||||
|
{
|
||||||
|
var previous = new DateTime(defaultYear.Value, defaultMonth.Value, 1).AddMonths(-1);
|
||||||
|
sourcePeriod = await _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.FirstOrDefaultAsync(p => p.Year == previous.Year && p.Month == previous.Month);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourcePeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("metadata")]
|
||||||
|
public async Task<IActionResult> GetMetadata([FromQuery] int? year, [FromQuery] int? month)
|
||||||
|
{
|
||||||
|
var categoriesQuery = _context.BudgetCategories
|
||||||
|
.Include(c => c.BudgetPeriod)
|
||||||
|
.Include(c => c.Items)
|
||||||
|
.ThenInclude(i => i.BudgetItemDefinition)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (year.HasValue)
|
||||||
|
categoriesQuery = categoriesQuery.Where(c => c.BudgetPeriod.Year == year.Value);
|
||||||
|
if (month.HasValue)
|
||||||
|
categoriesQuery = categoriesQuery.Where(c => c.BudgetPeriod.Month == month.Value);
|
||||||
|
|
||||||
|
var categories = await categoriesQuery.ToListAsync();
|
||||||
|
|
||||||
|
var categoryDefs = categories
|
||||||
|
.Where(c => c.BudgetCategoryDefinitionId.HasValue)
|
||||||
|
.GroupBy(c => c.Definition?.Name ?? c.Name)
|
||||||
|
.Select(g => new {
|
||||||
|
Name = g.Key,
|
||||||
|
Color = g.First().Color ?? "#cccccc" // direkt från kategorin
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var categoryLabels = categories
|
||||||
|
.GroupBy(c => c.Name)
|
||||||
|
.Select(g => new {
|
||||||
|
Name = g.Key,
|
||||||
|
Color = g.First().Color ?? "#cccccc"
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
|
var itemDefs = categories
|
||||||
|
.SelectMany(c => c.Items)
|
||||||
|
.Select(i => i.BudgetItemDefinition?.Name ?? i.Name)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var itemLabels = categories
|
||||||
|
.SelectMany(c => c.Items)
|
||||||
|
.Select(i => i.Name)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Metadata: {categories.Count} categories, {itemLabels.Count} items");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
CategoryDefinitions = categoryDefs,
|
||||||
|
CategoryLabels = categoryLabels,
|
||||||
|
ItemDefinitions = itemDefs,
|
||||||
|
ItemLabels = itemLabels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("report/spreadsheet")]
|
||||||
|
public async Task<IActionResult> GetBudgetReport(
|
||||||
|
[FromQuery] int? year = null,
|
||||||
|
[FromQuery] List<int>? itemDefinitionIds = null,
|
||||||
|
[FromQuery] List<int>? categoryDefinitionIds = null,
|
||||||
|
[FromQuery] string? itemLabel = null,
|
||||||
|
[FromQuery] string? categoryLabel = null,
|
||||||
|
[FromQuery] bool includeCategoryDefinitions = false,
|
||||||
|
[FromQuery] bool includeCategoryLabels = false,
|
||||||
|
[FromQuery] bool includeItemDefinitions = true,
|
||||||
|
[FromQuery] bool includeItemLabels = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ✅ Ladda navigationsproperties för definitions
|
||||||
|
var query = _context.BudgetPeriods
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Definition) // CategoryDefinition
|
||||||
|
.Include(p => p.Categories)
|
||||||
|
.ThenInclude(c => c.Items)
|
||||||
|
.ThenInclude(i => i.BudgetItemDefinition) // ItemDefinition
|
||||||
|
.Where(p => p.Year.HasValue && p.Month.HasValue)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (year.HasValue)
|
||||||
|
query = query.Where(p => p.Year == year);
|
||||||
|
|
||||||
|
var periods = await query
|
||||||
|
.OrderByDescending(p => p.Year)
|
||||||
|
.ThenByDescending(p => p.Month)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var reportData = periods.Select(p =>
|
||||||
|
{
|
||||||
|
var filteredCategories = p.Categories
|
||||||
|
.Where(c =>
|
||||||
|
(categoryDefinitionIds == null || categoryDefinitionIds.Count == 0 ||
|
||||||
|
(c.BudgetCategoryDefinitionId.HasValue &&
|
||||||
|
categoryDefinitionIds.Contains(c.BudgetCategoryDefinitionId.Value))) &&
|
||||||
|
(string.IsNullOrEmpty(categoryLabel) ||
|
||||||
|
(c.Name ?? string.Empty).Contains(categoryLabel, StringComparison.OrdinalIgnoreCase))
|
||||||
|
)
|
||||||
|
.ToList(); // Viktigt: gå över till LINQ to Objects
|
||||||
|
|
||||||
|
var filteredItems = filteredCategories
|
||||||
|
.SelectMany(c => c.Items)
|
||||||
|
.Where(i =>
|
||||||
|
i.IncludeInSummary &&
|
||||||
|
(itemDefinitionIds == null || itemDefinitionIds.Count == 0 ||
|
||||||
|
(i.BudgetItemDefinitionId.HasValue &&
|
||||||
|
itemDefinitionIds.Contains(i.BudgetItemDefinitionId.Value))) &&
|
||||||
|
(string.IsNullOrEmpty(itemLabel) ||
|
||||||
|
(i.Name ?? string.Empty).Contains(itemLabel, StringComparison.OrdinalIgnoreCase))
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 🔹 Skapa kolumner
|
||||||
|
var itemDefColumns = new Dictionary<string, decimal>();
|
||||||
|
var itemLabelColumns = new Dictionary<string, decimal>();
|
||||||
|
var catDefColumns = new Dictionary<string, decimal>();
|
||||||
|
var catLabelColumns = new Dictionary<string, decimal>();
|
||||||
|
|
||||||
|
if (includeItemDefinitions)
|
||||||
|
{
|
||||||
|
foreach (var g in filteredItems
|
||||||
|
.Where(i => i.BudgetItemDefinition != null)
|
||||||
|
.GroupBy(i => i.BudgetItemDefinition!.Name))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(g.Key))
|
||||||
|
itemDefColumns[g.Key] = g.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeItemLabels)
|
||||||
|
{
|
||||||
|
foreach (var g in filteredItems
|
||||||
|
.Where(i => !string.IsNullOrEmpty(i.Name))
|
||||||
|
.GroupBy(i => i.Name))
|
||||||
|
{
|
||||||
|
itemLabelColumns[g.Key] = g.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeCategoryDefinitions)
|
||||||
|
{
|
||||||
|
foreach (var g in filteredCategories
|
||||||
|
.Where(c => c.Definition != null)
|
||||||
|
.GroupBy(c => c.Definition!.Name))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(g.Key))
|
||||||
|
{
|
||||||
|
var total = g.SelectMany(c => c.Items)
|
||||||
|
.Where(i => i.IncludeInSummary)
|
||||||
|
.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
|
||||||
|
catDefColumns[g.Key] = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeCategoryLabels)
|
||||||
|
{
|
||||||
|
foreach (var g in filteredCategories
|
||||||
|
.Where(c => !string.IsNullOrEmpty(c.Name))
|
||||||
|
.GroupBy(c => c.Name))
|
||||||
|
{
|
||||||
|
var total = g.SelectMany(c => c.Items)
|
||||||
|
.Where(i => i.IncludeInSummary)
|
||||||
|
.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
|
||||||
|
catLabelColumns[g.Key] = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
id = p.Id,
|
||||||
|
year = p.Year ?? 0,
|
||||||
|
month = p.Month ?? 0,
|
||||||
|
income = filteredItems.Where(i => !i.IsExpense).Sum(i => i.Amount),
|
||||||
|
expense = filteredItems.Where(i => i.IsExpense).Sum(i => i.Amount),
|
||||||
|
net = filteredItems.Sum(i => i.IsExpense ? -i.Amount : i.Amount),
|
||||||
|
itemDefinitions = itemDefColumns,
|
||||||
|
itemLabels = itemLabelColumns,
|
||||||
|
categoryDefinitions = catDefColumns,
|
||||||
|
categoryLabels = catLabelColumns
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(reportData);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[GetBudgetReport] {ex}");
|
||||||
|
return StatusCode(500, $"Ett fel uppstod i rapportgenereringen: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Aberwyn.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Aberwyn.Controllers
|
namespace Aberwyn.Controllers
|
||||||
@@ -13,6 +14,11 @@ namespace Aberwyn.Controllers
|
|||||||
ViewBag.Month = month;
|
ViewBag.Month = month;
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
[Route("budget/list")]
|
||||||
|
public IActionResult List()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
[Route("budget/{name}")]
|
[Route("budget/{name}")]
|
||||||
public IActionResult Index(string name)
|
public IActionResult Index(string name)
|
||||||
@@ -21,6 +27,12 @@ namespace Aberwyn.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("budget/elkostnad")]
|
||||||
|
public IActionResult Elkostnad()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// För fallback när ingen månad/år anges
|
// För fallback när ingen månad/år anges
|
||||||
[Route("budget")]
|
[Route("budget")]
|
||||||
@@ -29,5 +41,7 @@ namespace Aberwyn.Controllers
|
|||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
return RedirectToAction("Index", new { year = now.Year, month = now.Month });
|
return RedirectToAction("Index", new { year = now.Year, month = now.Month });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,8 +220,28 @@ namespace Aberwyn.Controllers
|
|||||||
var menuService = _menuService;
|
var menuService = _menuService;
|
||||||
|
|
||||||
var today = DateTime.Today;
|
var today = DateTime.Today;
|
||||||
int resolvedWeek = week ?? ISOWeek.GetWeekOfYear(today);
|
|
||||||
int resolvedYear = year ?? today.Year;
|
// Starta alltid från ett GILTIGT datum
|
||||||
|
DateTime referenceDate;
|
||||||
|
|
||||||
|
if (week.HasValue && year.HasValue)
|
||||||
|
{
|
||||||
|
// Om week < 1 eller > 53 – tolka det som navigering
|
||||||
|
// från närliggande vecka i stället för att krascha
|
||||||
|
int safeWeek = Math.Clamp(week.Value, 1, 53);
|
||||||
|
referenceDate = ISOWeek.ToDateTime(year.Value, safeWeek, DayOfWeek.Monday);
|
||||||
|
|
||||||
|
// Justera datumet med differensen (ex: week=0 → -1 vecka)
|
||||||
|
referenceDate = referenceDate.AddDays((week.Value - safeWeek) * 7);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
referenceDate = today;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalisera alltid via datum
|
||||||
|
int resolvedWeek = ISOWeek.GetWeekOfYear(referenceDate);
|
||||||
|
int resolvedYear = ISOWeek.GetYear(referenceDate);
|
||||||
|
|
||||||
var menus = menuService.GetWeeklyMenu(resolvedWeek, resolvedYear);
|
var menus = menuService.GetWeeklyMenu(resolvedWeek, resolvedYear);
|
||||||
|
|
||||||
@@ -230,6 +250,11 @@ namespace Aberwyn.Controllers
|
|||||||
WeekNumber = resolvedWeek,
|
WeekNumber = resolvedWeek,
|
||||||
Year = resolvedYear,
|
Year = resolvedYear,
|
||||||
WeeklyMenus = menus,
|
WeeklyMenus = menus,
|
||||||
|
WishList = _context.MealWishes
|
||||||
|
.Include(w => w.RequestedByUser)
|
||||||
|
.Where(w => !w.IsArchived)
|
||||||
|
.OrderByDescending(w => w.CreatedAt)
|
||||||
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
var recent = menuService
|
var recent = menuService
|
||||||
@@ -353,7 +378,7 @@ namespace Aberwyn.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry.Cook = cook;
|
entry.Cook = cook;
|
||||||
|
entry.Date = FirstDateOfISOWeek(year, week).AddDays(day);
|
||||||
if (string.IsNullOrWhiteSpace(mealName))
|
if (string.IsNullOrWhiteSpace(mealName))
|
||||||
{
|
{
|
||||||
switch (mealType.ToLower())
|
switch (mealType.ToLower())
|
||||||
@@ -488,6 +513,108 @@ namespace Aberwyn.Controllers
|
|||||||
return RedirectToAction("PizzaAdmin");
|
return RedirectToAction("PizzaAdmin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = "Chef")]
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Calculator()
|
||||||
|
{
|
||||||
|
var plans = _context.DoughPlans
|
||||||
|
.OrderByDescending(p => p.Datum)
|
||||||
|
.ThenByDescending(p => p.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ViewBag.Plans = plans;
|
||||||
|
|
||||||
|
return View(new DoughPlan { AntalPizzor = 8, ViktPerPizza = 220 });
|
||||||
|
}
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetRecentMenuEntries(int weeksBack = 4)
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
|
||||||
|
// Datum för måndag i nuvarande vecka
|
||||||
|
int deltaToMonday = ((int)today.DayOfWeek + 6) % 7; // måndag=0, söndag=6
|
||||||
|
var thisWeekMonday = today.AddDays(-deltaToMonday);
|
||||||
|
|
||||||
|
// Startdatum: måndag X veckor bak
|
||||||
|
var startMonday = thisWeekMonday.AddDays(-7 * weeksBack);
|
||||||
|
|
||||||
|
// Slutdatum: söndag för förra veckan
|
||||||
|
var endSunday = thisWeekMonday.AddDays(-1);
|
||||||
|
|
||||||
|
// Hämta alla veckomenyer inom intervallet
|
||||||
|
var allMenus = _context.WeeklyMenus
|
||||||
|
.Where(w => w.Date >= startMonday && w.Date <= endSunday)
|
||||||
|
.OrderBy(w => w.Date)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Hämta alla relevanta meal IDs
|
||||||
|
var mealIds = allMenus
|
||||||
|
.SelectMany(w => new[] { w.BreakfastMealId, w.LunchMealId, w.DinnerMealId })
|
||||||
|
.Where(id => id.HasValue)
|
||||||
|
.Select(id => id!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var meals = _context.Meals
|
||||||
|
.Where(m => mealIds.Contains(m.Id))
|
||||||
|
.ToDictionary(m => m.Id, m => m.Name);
|
||||||
|
|
||||||
|
// Skapa entries
|
||||||
|
var entries = allMenus
|
||||||
|
.Select(w => new RecentMenuEntry
|
||||||
|
{
|
||||||
|
Date = w.Date,
|
||||||
|
WeekNumber = ISOWeek.GetWeekOfYear(w.Date),
|
||||||
|
Year = w.Date.Year,
|
||||||
|
BreakfastMealName = w.BreakfastMealId.HasValue && meals.ContainsKey(w.BreakfastMealId.Value)
|
||||||
|
? meals[w.BreakfastMealId.Value]
|
||||||
|
: "—",
|
||||||
|
LunchMealName = w.LunchMealId.HasValue && meals.ContainsKey(w.LunchMealId.Value)
|
||||||
|
? meals[w.LunchMealId.Value]
|
||||||
|
: "—",
|
||||||
|
DinnerMealName = w.DinnerMealId.HasValue && meals.ContainsKey(w.DinnerMealId.Value)
|
||||||
|
? meals[w.DinnerMealId.Value]
|
||||||
|
: "—"
|
||||||
|
})
|
||||||
|
.OrderBy(e => e.Date)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Json(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static DateTime FirstDateOfISOWeek(int year, int weekOfYear)
|
||||||
|
{
|
||||||
|
DateTime jan4 = new DateTime(year, 1, 4);
|
||||||
|
int daysOffset = DayOfWeek.Thursday - jan4.DayOfWeek;
|
||||||
|
|
||||||
|
DateTime firstThursday = jan4.AddDays(daysOffset);
|
||||||
|
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
|
||||||
|
int firstWeek = cal.GetWeekOfYear(firstThursday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
|
||||||
|
|
||||||
|
if (firstWeek <= 1)
|
||||||
|
weekOfYear -= 1;
|
||||||
|
|
||||||
|
DateTime result = firstThursday.AddDays(weekOfYear * 7);
|
||||||
|
return result.AddDays(-3); // tillbaka till måndag
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Authorize(Roles = "Chef")]
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult SaveDoughPlan([FromBody] DoughPlan model)
|
||||||
|
{
|
||||||
|
if (model == null) return BadRequest();
|
||||||
|
|
||||||
|
_context.DoughPlans.Add(model);
|
||||||
|
_context.SaveChanges();
|
||||||
|
|
||||||
|
return Json(new { success = true, id = model.Id });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,19 @@ namespace Aberwyn.Controllers
|
|||||||
//private readonly BudgetService _budgetService;
|
//private readonly BudgetService _budgetService;
|
||||||
private readonly MenuService _menuService;
|
private readonly MenuService _menuService;
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly TorrentService _torrentService;
|
||||||
|
|
||||||
|
|
||||||
// Constructor to inject dependencies
|
// Constructor to inject dependencies
|
||||||
public HomeController(ApplicationDbContext applicationDbContext, ILogger<HomeController> logger, MenuService menuService)
|
public HomeController(ApplicationDbContext applicationDbContext, ILogger<HomeController> logger, MenuService menuService, TorrentService torrentService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_menuService = menuService;
|
_menuService = menuService;
|
||||||
_context = applicationDbContext;
|
_context = applicationDbContext;
|
||||||
|
_torrentService = torrentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True";
|
var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True";
|
||||||
ViewBag.RestaurantIsOpen = isOpen;
|
ViewBag.RestaurantIsOpen = isOpen;
|
||||||
@@ -32,11 +34,17 @@ namespace Aberwyn.Controllers
|
|||||||
var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date;
|
var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date;
|
||||||
|
|
||||||
var todaysMenu = _menuService.GetMenuForDate(showDate);
|
var todaysMenu = _menuService.GetMenuForDate(showDate);
|
||||||
|
var userId = User.Identity?.Name ?? "guest";
|
||||||
|
|
||||||
|
// Awaita async-metoden
|
||||||
|
var newCount = await _torrentService.GetUnseenTorrentCountAsync(userId);
|
||||||
|
|
||||||
|
ViewBag.NewTorrentCount = newCount;
|
||||||
ViewBag.ShowDate = showDate;
|
ViewBag.ShowDate = showDate;
|
||||||
return View(todaysMenu);
|
return View(todaysMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IActionResult Privacy()
|
public IActionResult Privacy()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// MealMenuApiController.cs
|
// MealMenuApiController.cs
|
||||||
using Aberwyn.Models;
|
|
||||||
using Aberwyn.Data;
|
using Aberwyn.Data;
|
||||||
|
using Aberwyn.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
@@ -104,6 +105,10 @@ namespace Aberwyn.Controllers
|
|||||||
return StatusCode(500, "Failed to add meal.");
|
return StatusCode(500, "Failed to add meal.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Skolmat
|
#region Skolmat
|
||||||
[HttpGet("skolmat")]
|
[HttpGet("skolmat")]
|
||||||
public async Task<IActionResult> GetSkolmat(int week, [FromQuery] string sensor = "sensor.engelbrektsskolan")
|
public async Task<IActionResult> GetSkolmat(int week, [FromQuery] string sensor = "sensor.engelbrektsskolan")
|
||||||
|
|||||||
124
Aberwyn/Controllers/MealWishController.cs
Normal file
124
Aberwyn/Controllers/MealWishController.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using Aberwyn.Models; // Byt till din namespace
|
||||||
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class MealWishController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public MealWishController(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Skapa en ny önskan
|
||||||
|
[HttpPost("create")]
|
||||||
|
public async Task<ActionResult<MealWishDto>> Create([FromBody] CreateMealWishDto dto)
|
||||||
|
{
|
||||||
|
var wish = new MealWish
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
Recipe = dto.Recipe
|
||||||
|
};
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
|
||||||
|
wish.RequestedByUserId = userId;
|
||||||
|
wish.CreatedAt = DateTime.UtcNow;
|
||||||
|
wish.IsArchived = false;
|
||||||
|
wish.IsImported = false;
|
||||||
|
|
||||||
|
_context.MealWishes.Add(wish);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(MealWishDto.FromEntity(wish)); // ✅ returnera DTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Hämta alla önskningar (admin)
|
||||||
|
[HttpGet("all")]
|
||||||
|
public async Task<IActionResult> GetAll(bool includeArchived = false)
|
||||||
|
{
|
||||||
|
var wishes = await _context.MealWishes
|
||||||
|
.Include(w => w.RequestedByUser) // hämta användaren
|
||||||
|
.Where(w => includeArchived || !w.IsArchived)
|
||||||
|
.OrderByDescending(w => w.CreatedAt)
|
||||||
|
.Select(w => new {
|
||||||
|
w.Id,
|
||||||
|
w.Name,
|
||||||
|
w.Recipe,
|
||||||
|
RequestedByUserName = w.RequestedByUser != null ? w.RequestedByUser.UserName : "Okänd",
|
||||||
|
CreatedAt = w.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
|
||||||
|
w.IsArchived,
|
||||||
|
w.IsImported
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(wishes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Avfärda / arkivera
|
||||||
|
[HttpPost("{id}/archive")]
|
||||||
|
public async Task<IActionResult> Archive(int id)
|
||||||
|
{
|
||||||
|
var wish = await _context.MealWishes.FindAsync(id);
|
||||||
|
if (wish == null) return NotFound();
|
||||||
|
|
||||||
|
wish.IsArchived = true;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(wish);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Importera till Meals
|
||||||
|
[HttpPost("{id}/import")]
|
||||||
|
public async Task<IActionResult> Import(int id)
|
||||||
|
{
|
||||||
|
var wish = await _context.MealWishes.FindAsync(id);
|
||||||
|
if (wish == null) return NotFound();
|
||||||
|
|
||||||
|
if (wish.IsImported)
|
||||||
|
return BadRequest("Denna rätt har redan importerats.");
|
||||||
|
|
||||||
|
// Skapa en ny Meal
|
||||||
|
var meal = new Meal
|
||||||
|
{
|
||||||
|
Name = wish.Name,
|
||||||
|
Instructions = wish.Recipe,
|
||||||
|
IsAvailable = true,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
IsPublished = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Meals.Add(meal);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
wish.LinkedMealId = meal.Id;
|
||||||
|
wish.IsImported = true;
|
||||||
|
wish.IsArchived = true;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(wish);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<IActionResult> Search([FromQuery] string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return BadRequest();
|
||||||
|
|
||||||
|
var meals = await _context.Meals
|
||||||
|
.Where(m => m.Name.Contains(name))
|
||||||
|
.OrderBy(m => m.Name)
|
||||||
|
.Take(10)
|
||||||
|
.Select(m => new { m.Id, m.Name })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(meals);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,5 +59,8 @@ namespace Aberwyn.Controllers
|
|||||||
TempData["Success"] = "Beställningen har lagts!";
|
TempData["Success"] = "Beställningen har lagts!";
|
||||||
return RedirectToAction("Order");
|
return RedirectToAction("Order");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Aberwyn.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Aberwyn.Controllers
|
namespace Aberwyn.Controllers
|
||||||
@@ -6,9 +7,9 @@ namespace Aberwyn.Controllers
|
|||||||
[Authorize(Roles = "Budget")]
|
[Authorize(Roles = "Budget")]
|
||||||
public class ReportController : Controller
|
public class ReportController : Controller
|
||||||
{
|
{
|
||||||
public IActionResult BudgetReport()
|
public IActionResult Budget()
|
||||||
{
|
{
|
||||||
return View("BudgetReport");
|
return View(new BudgetReportViewModel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
245
Aberwyn/Controllers/RssController.cs
Normal file
245
Aberwyn/Controllers/RssController.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using BencodeNET.Torrents;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
public class RssController : Controller
|
||||||
|
{
|
||||||
|
private readonly ITorrentService _torrentService;
|
||||||
|
private readonly ILogger<RssController> _logger;
|
||||||
|
private readonly DelugeClient _deluge;
|
||||||
|
private readonly MovieMetadataService _movieMetadataService;
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public RssController(
|
||||||
|
ITorrentService torrentService,
|
||||||
|
ILogger<RssController> logger,
|
||||||
|
DelugeClient delugeClient,
|
||||||
|
MovieMetadataService movieMetadataService,
|
||||||
|
ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_torrentService = torrentService;
|
||||||
|
_logger = logger;
|
||||||
|
_deluge = delugeClient;
|
||||||
|
_movieMetadataService = movieMetadataService;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Index(int page = 1, string sort = "date", string range = "all")
|
||||||
|
{
|
||||||
|
var pageSize = 20;
|
||||||
|
var userId = User.Identity?.Name ?? "guest";
|
||||||
|
|
||||||
|
var torrents = await _torrentService.GetRecentTorrentsAsync(500);
|
||||||
|
|
||||||
|
// Filtrera på tidsintervall
|
||||||
|
torrents = range switch
|
||||||
|
{
|
||||||
|
"day" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-1)).ToList(),
|
||||||
|
"week" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-7)).ToList(),
|
||||||
|
"month" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddMonths(-1)).ToList(),
|
||||||
|
_ => torrents
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sortera
|
||||||
|
torrents = sort switch
|
||||||
|
{
|
||||||
|
"seeders" => torrents.OrderByDescending(t => t.Seeders).ToList(),
|
||||||
|
"leechers" => torrents.OrderByDescending(t => t.Leechers).ToList(),
|
||||||
|
"title" => torrents.OrderBy(t => t.MovieName).ToList(),
|
||||||
|
_ => torrents.OrderByDescending(t => t.PublishDate).ToList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hämta sedda torrents för användaren
|
||||||
|
var seenHashes = await _context.UserTorrentSeen
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => x.InfoHash)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Bygg viewmodels med IsNew
|
||||||
|
var pagedItems = torrents
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(t => new TorrentListItemViewModel
|
||||||
|
{
|
||||||
|
InfoHash = t.InfoHash ?? "",
|
||||||
|
Title = t.Title,
|
||||||
|
MovieName = t.MovieName ?? "",
|
||||||
|
PublishDate = t.PublishDate,
|
||||||
|
Seeders = t.Seeders,
|
||||||
|
Leechers = t.Leechers,
|
||||||
|
TorrentUrl = t.TorrentUrl,
|
||||||
|
Metadata = t.Metadata,
|
||||||
|
IsDownloaded = t.IsDownloaded,
|
||||||
|
IsNew = t.InfoHash != null && !seenHashes.Contains(t.InfoHash),
|
||||||
|
AvailableOn = !string.IsNullOrEmpty(t.Metadata?.Providers)
|
||||||
|
? t.Metadata.Providers.Split(',').ToList()
|
||||||
|
: new List<string>()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Markera som sedda
|
||||||
|
var newSeen = pagedItems
|
||||||
|
.Where(i => i.IsNew && !string.IsNullOrEmpty(i.InfoHash))
|
||||||
|
.Select(i => new UserTorrentSeen
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
InfoHash = i.InfoHash,
|
||||||
|
SeenDate = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.UserTorrentSeen.AddRange(newSeen);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var vm = new TorrentListViewModel
|
||||||
|
{
|
||||||
|
Items = pagedItems,
|
||||||
|
CurrentPage = page,
|
||||||
|
TotalPages = (int)Math.Ceiling(torrents.Count / (double)pageSize),
|
||||||
|
CurrentSort = sort,
|
||||||
|
CurrentRange = range
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Add(string torrentUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await _deluge.LoginAsync("deluge1"))
|
||||||
|
{
|
||||||
|
var success = await _deluge.AddTorrentUrlAsync(torrentUrl);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
// Hämta torrenten baserat på TorrentUrl
|
||||||
|
var torrent = await _context.Set<TorrentItem>()
|
||||||
|
.FirstOrDefaultAsync(t => t.TorrentUrl == torrentUrl);
|
||||||
|
|
||||||
|
if (torrent != null)
|
||||||
|
{
|
||||||
|
// Markera som nerladdad
|
||||||
|
torrent.IsDownloaded = true;
|
||||||
|
torrent.Status = TorrentStatus.Downloaded;
|
||||||
|
_context.Update(torrent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Skapa ny post om den inte finns
|
||||||
|
var newTorrent = new TorrentItem
|
||||||
|
{
|
||||||
|
Title = torrentUrl, // byt till korrekt namnkälla om du har
|
||||||
|
TorrentUrl = torrentUrl,
|
||||||
|
PublishDate = DateTime.UtcNow,
|
||||||
|
Status = TorrentStatus.Downloaded,
|
||||||
|
IsDownloaded = true,
|
||||||
|
RssSource = "Manuell", // eller sätt rätt källa
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Add(newTorrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("SavedChanges to torrents");
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "Torrent tillagd i Deluge och markerad som nerladdad."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new { success = false, message = "Misslyckades att lägga till torrent i Deluge." });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Fel vid tillägg av torrent");
|
||||||
|
return Json(new { success = false, message = "Ett fel uppstod vid tillägg av torrent." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Upload(TorrentUploadViewModel model)
|
||||||
|
{
|
||||||
|
if (model.TorrentFile == null || model.TorrentFile.Length == 0)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("TorrentFile", "Vänligen välj en torrent-fil");
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.TorrentFile.FileName.EndsWith(".torrent", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("TorrentFile", "Endast .torrent filer är tillåtna");
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.TorrentFile.Length > 10 * 1024 * 1024) // 10MB limit
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("TorrentFile", "Filen är för stor (max 10MB)");
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var torrentInfo = await _torrentService.ParseTorrentAsync(model.TorrentFile);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(torrentInfo.ErrorMessage))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("", torrentInfo.ErrorMessage);
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
|
||||||
|
|
||||||
|
model.TorrentInfo = torrentInfo;
|
||||||
|
model.ShowResults = true;
|
||||||
|
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Fel vid uppladdning av torrent");
|
||||||
|
ModelState.AddModelError("", "Ett oväntat fel inträffade");
|
||||||
|
return View("Index", model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RefreshStats(string infoHash, string scrapeUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var torrentInfo = new TorrentInfo
|
||||||
|
{
|
||||||
|
InfoHash = infoHash,
|
||||||
|
ScrapeUrl = scrapeUrl,
|
||||||
|
InfoHashBytes = Convert.FromHexString(infoHash.Replace("%", ""))
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = updatedInfo.HasTrackerData,
|
||||||
|
seeders = updatedInfo.Seeders,
|
||||||
|
leechers = updatedInfo.Leechers,
|
||||||
|
completed = updatedInfo.Completed,
|
||||||
|
error = updatedInfo.ErrorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Fel vid uppdatering av tracker-stats");
|
||||||
|
return Json(new { success = false, error = "Fel vid uppdatering" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,11 +15,15 @@ namespace Aberwyn.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IWebHostEnvironment _env;
|
private readonly IWebHostEnvironment _env;
|
||||||
private readonly ILogger<SetupController> _logger;
|
private readonly ILogger<SetupController> _logger;
|
||||||
|
private readonly string _filePath;
|
||||||
|
|
||||||
public SetupController(IWebHostEnvironment env, ILogger<SetupController> logger)
|
public SetupController(IWebHostEnvironment env, ILogger<SetupController> logger)
|
||||||
{
|
{
|
||||||
_env = env;
|
_env = env;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
var dataRoot = Path.Combine(Directory.GetCurrentDirectory(), "data"); // /app/data i containern
|
||||||
|
_filePath = Path.Combine(dataRoot, "infrastructure", "setup.json");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnActionExecuting(ActionExecutingContext context)
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
@@ -35,7 +39,7 @@ namespace Aberwyn.Controllers
|
|||||||
[HttpPost("reset")]
|
[HttpPost("reset")]
|
||||||
public IActionResult Reset()
|
public IActionResult Reset()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
|
var path = _filePath;
|
||||||
|
|
||||||
var resetSettings = new SetupSettings
|
var resetSettings = new SetupSettings
|
||||||
{
|
{
|
||||||
@@ -70,6 +74,7 @@ namespace Aberwyn.Controllers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Start setup write");
|
||||||
// Bygg connection string säkert
|
// Bygg connection string säkert
|
||||||
var baseConnBuilder = new MySqlConnectionStringBuilder
|
var baseConnBuilder = new MySqlConnectionStringBuilder
|
||||||
{
|
{
|
||||||
@@ -122,12 +127,10 @@ namespace Aberwyn.Controllers
|
|||||||
|
|
||||||
// Sätt konfig-flagga tidigt
|
// Sätt konfig-flagga tidigt
|
||||||
model.IsConfigured = true;
|
model.IsConfigured = true;
|
||||||
|
_logger.LogDebug("Filepath" + _filePath);
|
||||||
// Spara setup.json
|
// Spara setup.json
|
||||||
var filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
|
|
||||||
var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
|
||||||
System.IO.File.WriteAllText(filePath, json);
|
System.IO.File.WriteAllText(_filePath, json);
|
||||||
|
|
||||||
// Roller och admin
|
// Roller och admin
|
||||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
public class TorrentController : Controller
|
|
||||||
{
|
|
||||||
private readonly ITorrentService _torrentService;
|
|
||||||
private readonly ILogger<TorrentController> _logger;
|
|
||||||
|
|
||||||
public TorrentController(ITorrentService torrentService, ILogger<TorrentController> logger)
|
|
||||||
{
|
|
||||||
_torrentService = torrentService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return View(new TorrentUploadViewModel());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> Upload(TorrentUploadViewModel model)
|
|
||||||
{
|
|
||||||
if (model.TorrentFile == null || model.TorrentFile.Length == 0)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("TorrentFile", "Vänligen välj en torrent-fil");
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!model.TorrentFile.FileName.EndsWith(".torrent", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("TorrentFile", "Endast .torrent filer är tillåtna");
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.TorrentFile.Length > 10 * 1024 * 1024) // 10MB limit
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("TorrentFile", "Filen är för stor (max 10MB)");
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parsa torrent-filen
|
|
||||||
var torrentInfo = await _torrentService.ParseTorrentAsync(model.TorrentFile);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(torrentInfo.ErrorMessage))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("", torrentInfo.ErrorMessage);
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Försök hämta tracker-statistik
|
|
||||||
torrentInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
|
|
||||||
|
|
||||||
model.TorrentInfo = torrentInfo;
|
|
||||||
model.ShowResults = true;
|
|
||||||
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Fel vid uppladdning av torrent");
|
|
||||||
ModelState.AddModelError("", "Ett oväntat fel inträffade");
|
|
||||||
return View("Index", model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> RefreshStats(string infoHash, string scrapeUrl)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var torrentInfo = new TorrentInfo
|
|
||||||
{
|
|
||||||
InfoHash = infoHash,
|
|
||||||
ScrapeUrl = scrapeUrl,
|
|
||||||
InfoHashBytes = Convert.FromHexString(infoHash.Replace("%", ""))
|
|
||||||
};
|
|
||||||
|
|
||||||
var updatedInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
|
|
||||||
|
|
||||||
return Json(new
|
|
||||||
{
|
|
||||||
success = updatedInfo.HasTrackerData,
|
|
||||||
seeders = updatedInfo.Seeders,
|
|
||||||
leechers = updatedInfo.Leechers,
|
|
||||||
completed = updatedInfo.Completed,
|
|
||||||
error = updatedInfo.ErrorMessage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Fel vid uppdatering av tracker-stats");
|
|
||||||
return Json(new { success = false, error = "Fel vid uppdatering" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Aberwyn.Models;
|
using Aberwyn.Models;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
|
||||||
namespace Aberwyn.Data
|
namespace Aberwyn.Data
|
||||||
{
|
{
|
||||||
@@ -14,7 +15,13 @@ namespace Aberwyn.Data
|
|||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
builder.Entity<TorrentItem>()
|
||||||
|
.HasIndex(t => t.InfoHash)
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
builder.Entity<TorrentItem>()
|
||||||
|
.Property(t => t.Status)
|
||||||
|
.HasConversion<string>();
|
||||||
builder.Entity<WeeklyMenu>().ToTable("WeeklyMenu");
|
builder.Entity<WeeklyMenu>().ToTable("WeeklyMenu");
|
||||||
builder.Entity<MealCategory>().HasData(
|
builder.Entity<MealCategory>().HasData(
|
||||||
new MealCategory
|
new MealCategory
|
||||||
@@ -28,9 +35,13 @@ namespace Aberwyn.Data
|
|||||||
DisplayOrder = 1
|
DisplayOrder = 1
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
builder.Entity<TorrentItem>()
|
||||||
|
.OwnsOne(t => t.Metadata);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public DbSet<DoughPlan> DoughPlans { get; set; }
|
||||||
|
|
||||||
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
|
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
|
||||||
public DbSet<BudgetCategory> BudgetCategories { get; set; }
|
public DbSet<BudgetCategory> BudgetCategories { get; set; }
|
||||||
@@ -52,6 +63,11 @@ namespace Aberwyn.Data
|
|||||||
public DbSet<LabIngredient> LabIngredients { get; set; }
|
public DbSet<LabIngredient> LabIngredients { get; set; }
|
||||||
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
|
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
|
||||||
public DbSet<MealRating> MealRatings { get; set; }
|
public DbSet<MealRating> MealRatings { get; set; }
|
||||||
|
public DbSet<TorrentItem> TorrentItems { get; set; }
|
||||||
|
public DbSet<RssFeed> RssFeeds { get; set; }
|
||||||
|
public DbSet<DownloadRule> DownloadRules { get; set; }
|
||||||
|
public DbSet<UserTorrentSeen> UserTorrentSeen { get; set; }
|
||||||
|
public DbSet<MealWish> MealWishes { get; set; }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
79
Aberwyn/Data/DelugeClient.cs
Normal file
79
Aberwyn/Data/DelugeClient.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using MySqlX.XDevAPI;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public class DelugeClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly string _url;
|
||||||
|
private string _sessionId;
|
||||||
|
|
||||||
|
public DelugeClient(HttpClient httpClient, string baseUrl = "http://192.168.1.3:8112/json")
|
||||||
|
{
|
||||||
|
_http = httpClient;
|
||||||
|
_url = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LoginAsync(string password)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
method = "auth.login",
|
||||||
|
@params = new object[] { password },
|
||||||
|
id = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _http.PostAsJsonAsync(_url, payload);
|
||||||
|
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
|
||||||
|
// spara sessioncookie för framtida requests
|
||||||
|
if (response.Headers.TryGetValues("Set-Cookie", out var cookies))
|
||||||
|
{
|
||||||
|
_sessionId = cookies.FirstOrDefault()?.Split(';')[0];
|
||||||
|
if (!_http.DefaultRequestHeaders.Contains("Cookie"))
|
||||||
|
_http.DefaultRequestHeaders.Add("Cookie", _sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.GetProperty("result").GetBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddMagnetAsync(string magnetLink)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
method = "core.add_torrent_url",
|
||||||
|
@params = new object[] { magnetLink, new { } },
|
||||||
|
id = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _http.PostAsJsonAsync(_url, payload);
|
||||||
|
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
return json.GetProperty("result").ValueKind != JsonValueKind.Null;
|
||||||
|
}
|
||||||
|
public async Task<bool> AddTorrentUrlAsync(string torrentUrl)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
method = "core.add_torrent_url",
|
||||||
|
@params = new object[]
|
||||||
|
{
|
||||||
|
torrentUrl,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
download_location = "/download/incomplete",
|
||||||
|
move_completed = true,
|
||||||
|
move_completed_path = "/media/Movies",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _http.PostAsJsonAsync(_url, payload);
|
||||||
|
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
|
||||||
|
return json.GetProperty("result").ValueKind != JsonValueKind.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
103
Aberwyn/Data/HDTorrentsTrackerScraper.cs
Normal file
103
Aberwyn/Data/HDTorrentsTrackerScraper.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public class HdTorrentsTrackerScraper
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<HdTorrentsTrackerScraper> _logger;
|
||||||
|
|
||||||
|
public HdTorrentsTrackerScraper(HttpClient httpClient, ILogger<HdTorrentsTrackerScraper> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(int seeders, int leechers, int completed)> ScrapeHdTorrents(string infoHash, string downloadKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(infoHash) || string.IsNullOrEmpty(downloadKey))
|
||||||
|
return (0, 0, 0);
|
||||||
|
|
||||||
|
// Konvertera hex InfoHash till URL-encoded bytes
|
||||||
|
var infoHashBytes = HexStringToBytes(infoHash);
|
||||||
|
|
||||||
|
//%67%2e%d6%f9%30%e9%33%88%e3%1b%c6%f0%85%f5%f7%78%44%05%10%e7
|
||||||
|
var encodedInfoHash = Uri.EscapeDataString(Encoding.GetEncoding("ISO-8859-1").GetString(infoHashBytes));
|
||||||
|
var encodedInfoHashv2 = EncodeInfoHash(infoHash);
|
||||||
|
|
||||||
|
// Bygga scrape URL baserat på HD-Torrents format
|
||||||
|
var scrapeUrl = $"https://hdts-announce.ru/scrape.php?pid=98d498dedff78ba0334f662d151eb19b7?info_hash={encodedInfoHashv2}";
|
||||||
|
|
||||||
|
_logger.LogInformation("Scraping HD-Torrents: {Url}", scrapeUrl);
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync(scrapeUrl);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(); // Fix: Content.ReadAsByteArrayAsync()
|
||||||
|
return ParseBencodeResponse(content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Scrape failed with status: {StatusCode}", response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to scrape HD-Torrents for InfoHash: {InfoHash}", infoHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, 0, 0);
|
||||||
|
}
|
||||||
|
private static string EncodeInfoHash(string hex)
|
||||||
|
{
|
||||||
|
var bytes = HexStringToBytes(hex);
|
||||||
|
return string.Concat(bytes.Select(b => $"%{b:X2}")).ToLower();
|
||||||
|
}
|
||||||
|
private static byte[] HexStringToBytes(string hex)
|
||||||
|
{
|
||||||
|
if (hex.Length % 2 != 0)
|
||||||
|
throw new ArgumentException("Invalid hex string length.");
|
||||||
|
|
||||||
|
var bytes = new byte[hex.Length / 2];
|
||||||
|
for (int i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int seeders, int leechers, int completed) ParseBencodeResponse(byte[] data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Enkel bencode parsing för tracker response
|
||||||
|
var response = System.Text.Encoding.UTF8.GetString(data);
|
||||||
|
|
||||||
|
// Tracker response är vanligtvis i format:
|
||||||
|
// d5:filesd20:[info_hash]d8:completei[antal]e9:downloadedi[antal]e10:incompletei[antal]eee
|
||||||
|
|
||||||
|
// Hitta complete (seeders)
|
||||||
|
var completeMatch = System.Text.RegularExpressions.Regex.Match(response, @"8:completei(\d+)e");
|
||||||
|
var seeders = completeMatch.Success ? int.Parse(completeMatch.Groups[1].Value) : 0;
|
||||||
|
|
||||||
|
// Hitta incomplete (leechers)
|
||||||
|
var incompleteMatch = System.Text.RegularExpressions.Regex.Match(response, @"10:incompletei(\d+)e");
|
||||||
|
var leechers = incompleteMatch.Success ? int.Parse(incompleteMatch.Groups[1].Value) : 0;
|
||||||
|
|
||||||
|
// Hitta downloaded (completed)
|
||||||
|
var downloadedMatch = System.Text.RegularExpressions.Regex.Match(response, @"9:downloadedi(\d+)e");
|
||||||
|
var completed = downloadedMatch.Success ? int.Parse(downloadedMatch.Groups[1].Value) : 0;
|
||||||
|
|
||||||
|
return (seeders, leechers, completed);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to parse bencode response");
|
||||||
|
return (0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Aberwyn/Data/IRssProcessor.cs
Normal file
7
Aberwyn/Data/IRssProcessor.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public interface IRssProcessor
|
||||||
|
{
|
||||||
|
Task ProcessRssFeeds();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -435,6 +435,8 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
|
|||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public List<MealCategory> GetMealCategories()
|
public List<MealCategory> GetMealCategories()
|
||||||
{
|
{
|
||||||
return _context.MealCategories.OrderBy(c => c.DisplayOrder).ToList();
|
return _context.MealCategories.OrderBy(c => c.DisplayOrder).ToList();
|
||||||
|
|||||||
104
Aberwyn/Data/MovieMetadataService.cs
Normal file
104
Aberwyn/Data/MovieMetadataService.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public class MovieMetadataService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly string _apiKey = "6a666b45";
|
||||||
|
private readonly string _tpiKey = "6a666b45";
|
||||||
|
|
||||||
|
|
||||||
|
public MovieMetadataService(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_http = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MovieMetadata?> GetMovieAsync(string title, int? year = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MovieMetadata? metadata = null;
|
||||||
|
|
||||||
|
// Först försök med titel + år
|
||||||
|
if (year.HasValue)
|
||||||
|
{
|
||||||
|
var urlWithYear = $"https://www.omdbapi.com/?t={Uri.EscapeDataString(title)}&y={year.Value}&apikey={_apiKey}";
|
||||||
|
metadata = await _http.GetFromJsonAsync<MovieMetadata>(urlWithYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Om inget hittas, försök bara med titel
|
||||||
|
if (metadata == null || string.IsNullOrEmpty(metadata.Title))
|
||||||
|
{
|
||||||
|
var urlTitleOnly = $"https://www.omdbapi.com/?t={Uri.EscapeDataString(title)}&apikey={_apiKey}";
|
||||||
|
metadata = await _http.GetFromJsonAsync<MovieMetadata>(urlTitleOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returnera metadata om något hittades
|
||||||
|
return (metadata != null && !string.IsNullOrEmpty(metadata.Title)) ? metadata : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TmdbService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http = new();
|
||||||
|
private readonly string _apiKey = "aef2f49296b77b9b9c269678d04bdbc6";
|
||||||
|
private readonly string _country = "SE";
|
||||||
|
|
||||||
|
public async Task<string> GetWatchProvidersByTitleAsync(string title, int? year = null)
|
||||||
|
{
|
||||||
|
var query = Uri.EscapeDataString(title);
|
||||||
|
var url = $"https://api.themoviedb.org/3/search/movie?api_key={_apiKey}&query={query}";
|
||||||
|
if (year.HasValue)
|
||||||
|
url += $"&year={year.Value}";
|
||||||
|
|
||||||
|
var searchResult = await _http.GetFromJsonAsync<TmdbSearchResponse>(url);
|
||||||
|
var movie = searchResult?.Results?.FirstOrDefault();
|
||||||
|
if (movie == null) return "";
|
||||||
|
|
||||||
|
var providersUrl = $"https://api.themoviedb.org/3/movie/{movie.Id}/watch/providers?api_key={_apiKey}";
|
||||||
|
var providersResult = await _http.GetFromJsonAsync<WatchProvidersResponse>(providersUrl);
|
||||||
|
|
||||||
|
if (providersResult?.Results?.ContainsKey(_country) == true)
|
||||||
|
{
|
||||||
|
var seProviders = providersResult.Results[_country];
|
||||||
|
var names = new List<string>();
|
||||||
|
if (seProviders.Flatrate != null) names.AddRange(seProviders.Flatrate.Select(p => p.ProviderName));
|
||||||
|
//if (seProviders.Rent != null) names.AddRange(seProviders.Rent.Select(p => p.ProviderName));
|
||||||
|
//if (seProviders.Buy != null) names.AddRange(seProviders.Buy.Select(p => p.ProviderName));
|
||||||
|
return string.Join(", ", names.Distinct());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-klasser för TMDb
|
||||||
|
public class TmdbSearchResponse { public List<TmdbMovie> Results { get; set; } = new(); }
|
||||||
|
public class TmdbMovie { public int Id { get; set; } }
|
||||||
|
public class WatchProvidersResponse { public Dictionary<string, CountryProviders> Results { get; set; } = new(); }
|
||||||
|
public class CountryProviders
|
||||||
|
{
|
||||||
|
public List<Provider>? Flatrate { get; set; }
|
||||||
|
public List<Provider>? Rent { get; set; }
|
||||||
|
public List<Provider>? Buy { get; set; }
|
||||||
|
}
|
||||||
|
public class Provider
|
||||||
|
{
|
||||||
|
[JsonPropertyName("provider_name")]
|
||||||
|
public string ProviderName { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
304
Aberwyn/Data/RssProcessor.cs
Normal file
304
Aberwyn/Data/RssProcessor.cs
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using Aberwyn.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public class RssProcessor : IRssProcessor
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly ILogger<RssProcessor> _logger;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly HdTorrentsTrackerScraper _trackerScraper;
|
||||||
|
private readonly MovieMetadataService _movieMetadataService;
|
||||||
|
|
||||||
|
public RssProcessor(ApplicationDbContext context, ILogger<RssProcessor> logger,
|
||||||
|
HttpClient httpClient, HdTorrentsTrackerScraper trackerScraper, MovieMetadataService movieMetadataService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_trackerScraper = trackerScraper;
|
||||||
|
_movieMetadataService = movieMetadataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ProcessRssFeeds()
|
||||||
|
{
|
||||||
|
|
||||||
|
var debug = false;
|
||||||
|
|
||||||
|
var oneHourAgo = DateTime.UtcNow.AddHours(-1);
|
||||||
|
if (debug)
|
||||||
|
oneHourAgo = DateTime.UtcNow.AddHours(1);
|
||||||
|
|
||||||
|
var activeFeeds = await _context.RssFeeds
|
||||||
|
.Where(f => f.IsActive && f.LastChecked <= oneHourAgo)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var feed in activeFeeds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessSingleFeed(feed);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing RSS feed {FeedName}", feed.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessSingleFeed(RssFeed feed)
|
||||||
|
{
|
||||||
|
var rssContent = await _httpClient.GetStringAsync(feed.Url);
|
||||||
|
var rssDoc = XDocument.Parse(rssContent);
|
||||||
|
var items = rssDoc.Descendants("item");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var torrentItem = ParseRssItem(item, feed);
|
||||||
|
Console.WriteLine($"Trying to save: Title='{torrentItem.Title}', InfoHash='{torrentItem.InfoHash}', MovieName='{torrentItem.MovieName}'");
|
||||||
|
|
||||||
|
// Kolla om den redan finns
|
||||||
|
var exists = await _context.TorrentItems
|
||||||
|
.AnyAsync(t => t.InfoHash == torrentItem.InfoHash ||
|
||||||
|
(t.Title == torrentItem.Title && t.RssSource == feed.Name));
|
||||||
|
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(torrentItem.InfoHash) && !string.IsNullOrEmpty(torrentItem.DownloadKey))
|
||||||
|
{
|
||||||
|
var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents(
|
||||||
|
torrentItem.InfoHash,
|
||||||
|
torrentItem.DownloadKey);
|
||||||
|
|
||||||
|
torrentItem.Seeders = seeders;
|
||||||
|
torrentItem.Leechers = leechers;
|
||||||
|
torrentItem.Completed = completed;
|
||||||
|
|
||||||
|
Console.WriteLine($"Scraped stats for {torrentItem.Title}: S:{seeders} L:{leechers} C:{completed}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = await _movieMetadataService.GetMovieAsync(torrentItem.MovieName, torrentItem.Year);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
torrentItem.Metadata = metadata;
|
||||||
|
var tmdbService = new TmdbService();
|
||||||
|
torrentItem.Metadata.Providers = await tmdbService.GetWatchProvidersByTitleAsync(torrentItem.MovieName, torrentItem.Year);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.TorrentItems.Add(torrentItem);
|
||||||
|
var savedChanges = await _context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"SaveChanges returned: {savedChanges}");
|
||||||
|
// Kolla auto-download regler
|
||||||
|
if (await ShouldAutoDownload(torrentItem))
|
||||||
|
{
|
||||||
|
await ProcessTorrentDownload(torrentItem);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
|
||||||
|
var howLongAgo = DateTime.UtcNow.AddHours(-6);
|
||||||
|
if (torrentItem.PublishDate >= howLongAgo)
|
||||||
|
{
|
||||||
|
|
||||||
|
var existing = await _context.TorrentItems
|
||||||
|
.FirstOrDefaultAsync(t => t.InfoHash == torrentItem.InfoHash
|
||||||
|
|| (t.Title == torrentItem.Title && t.RssSource == feed.Name));
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents(
|
||||||
|
existing.InfoHash,
|
||||||
|
existing.DownloadKey);
|
||||||
|
|
||||||
|
existing.Seeders = seeders;
|
||||||
|
existing.Leechers = leechers;
|
||||||
|
existing.Completed = completed;
|
||||||
|
|
||||||
|
_context.TorrentItems.Update(existing);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
Console.WriteLine($"Updated stats for {existing.Title}: S:{seeders} L:{leechers} C:{completed}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception itemEx)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error processing individual item: {itemEx.Message}");
|
||||||
|
Console.WriteLine($"Stack trace: {itemEx.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.LastChecked = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error in ProcessSingleFeed: {ex.Message}");
|
||||||
|
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TorrentItem ParseRssItem(XElement item, RssFeed feed)
|
||||||
|
{
|
||||||
|
var title = item.Element("title")?.Value ?? "Unknown Title";
|
||||||
|
var description = item.Element("description")?.Value ?? ""; // Fix: Default till tom sträng
|
||||||
|
var pubDate = DateTime.TryParse(item.Element("pubDate")?.Value, out var date) ? date : DateTime.UtcNow;
|
||||||
|
var link = item.Element("link")?.Value ?? "";
|
||||||
|
|
||||||
|
var infoHash = ExtractInfoHashFromUrl(link);
|
||||||
|
var downloadKey = ExtractParameterFromUrl(link, "key");
|
||||||
|
var token = ExtractParameterFromUrl(link, "token");
|
||||||
|
var (movieName, year) = ParseMovieNameAndYear(title);
|
||||||
|
|
||||||
|
var magnetLink = "";
|
||||||
|
if (!string.IsNullOrEmpty(infoHash))
|
||||||
|
{
|
||||||
|
magnetLink = $"magnet:?xt=urn:btih:{infoHash}&dn={Uri.EscapeDataString(title)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TorrentItem
|
||||||
|
{
|
||||||
|
Title = title ?? "Unknown Title", // Garanterat inte null
|
||||||
|
Description = description ?? string.Empty, // Garanterat inte null
|
||||||
|
TorrentUrl = string.IsNullOrEmpty(link) ? null : link,
|
||||||
|
MagnetLink = string.IsNullOrEmpty(magnetLink) ? null : magnetLink,
|
||||||
|
InfoHash = string.IsNullOrEmpty(infoHash) ? null : infoHash,
|
||||||
|
PublishDate = pubDate,
|
||||||
|
RssSource = feed.Name ?? "Unknown", // Garanterat inte null
|
||||||
|
Status = TorrentStatus.New,
|
||||||
|
DownloadKey = string.IsNullOrEmpty(downloadKey) ? null : downloadKey,
|
||||||
|
Token = string.IsNullOrEmpty(token) ? null : token,
|
||||||
|
MovieName = string.IsNullOrEmpty(movieName) ? null : movieName,
|
||||||
|
Year = year,
|
||||||
|
Category = DetermineCategory(title),
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string movieName, int? year) ParseMovieNameAndYear(string title)
|
||||||
|
{
|
||||||
|
// Exempel titlar:
|
||||||
|
// "Bring It on Fight to the Finish 2009 BluRay 1080p DDP 5.1 x264-hallowed"
|
||||||
|
// "Deadpool & Wolverine 2024 Hybrid 1080p UHD BluRay DD+5.1 Atmos DV HDR10+ x265-HiDt"
|
||||||
|
|
||||||
|
var movieName = "";
|
||||||
|
int? year = null;
|
||||||
|
|
||||||
|
// Leta efter år (4 siffror mellan 1900-2099)
|
||||||
|
var yearMatch = System.Text.RegularExpressions.Regex.Match(title, @"\b(19\d{2}|20\d{2})\b");
|
||||||
|
if (yearMatch.Success && int.TryParse(yearMatch.Groups[1].Value, out var parsedYear))
|
||||||
|
{
|
||||||
|
year = parsedYear;
|
||||||
|
|
||||||
|
// Ta allt före året som filmnamn
|
||||||
|
var yearIndex = yearMatch.Index;
|
||||||
|
movieName = title.Substring(0, yearIndex).Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Om inget år hittas, ta första delen före vanliga release-keywords
|
||||||
|
var keywordMatch = System.Text.RegularExpressions.Regex.Match(title,
|
||||||
|
@"\b(BluRay|WEB-DL|WEBRip|HDTV|DVDRIP|REMUX|UHD|1080p|720p|480p|2160p)\b",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (keywordMatch.Success)
|
||||||
|
{
|
||||||
|
movieName = title.Substring(0, keywordMatch.Index).Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: ta första delen innan sista bindestreck
|
||||||
|
var lastDashIndex = title.LastIndexOf('-');
|
||||||
|
if (lastDashIndex > 0)
|
||||||
|
{
|
||||||
|
movieName = title.Substring(0, lastDashIndex).Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
movieName = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rensa bort vanliga suffix från filmnamnet
|
||||||
|
movieName = System.Text.RegularExpressions.Regex.Replace(movieName,
|
||||||
|
@"\b(REMASTERED|REPACK|PROPER|REAL|EXTENDED|DIRECTORS?\.?CUT|UNRATED)\b",
|
||||||
|
"", RegexOptions.IgnoreCase).Trim();
|
||||||
|
|
||||||
|
return (movieName, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractInfoHashFromUrl(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url)) return "";
|
||||||
|
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(url, @"hash=([a-fA-F0-9]{40})");
|
||||||
|
return match.Success ? match.Groups[1].Value.ToUpper() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractParameterFromUrl(string url, string paramName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url)) return "";
|
||||||
|
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(url, $@"{paramName}=([^&]+)");
|
||||||
|
return match.Success ? Uri.UnescapeDataString(match.Groups[1].Value) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DetermineCategory(string title)
|
||||||
|
{
|
||||||
|
if (System.Text.RegularExpressions.Regex.IsMatch(title, @"\b(S\d{2}E\d{2}|Season|Episode)\b", RegexOptions.IgnoreCase))
|
||||||
|
return "TV";
|
||||||
|
else
|
||||||
|
return "Movies";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShouldAutoDownload(TorrentItem item)
|
||||||
|
{
|
||||||
|
var rules = await _context.DownloadRules.Where(r => r.AutoDownload).ToListAsync();
|
||||||
|
|
||||||
|
return rules.Any(rule =>
|
||||||
|
(string.IsNullOrEmpty(rule.KeywordFilter) ||
|
||||||
|
item.Title.Contains(rule.KeywordFilter, StringComparison.OrdinalIgnoreCase)) &&
|
||||||
|
(string.IsNullOrEmpty(rule.CategoryFilter) ||
|
||||||
|
item.Category == rule.CategoryFilter) &&
|
||||||
|
item.Seeders >= rule.MinSeeders &&
|
||||||
|
(rule.MaxSize == 0 || item.Size <= rule.MaxSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessTorrentDownload(TorrentItem item)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(item.MagnetLink))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting magnet download for {Title}", item.Title);
|
||||||
|
item.Status = TorrentStatus.Downloaded;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(item.TorrentUrl))
|
||||||
|
{
|
||||||
|
var torrentData = await _httpClient.GetByteArrayAsync(item.TorrentUrl);
|
||||||
|
item.Status = TorrentStatus.Downloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error downloading torrent {Title}", item.Title);
|
||||||
|
item.Status = TorrentStatus.Failed;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ namespace Aberwyn.Data
|
|||||||
public SetupService(IWebHostEnvironment env)
|
public SetupService(IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
_env = env;
|
_env = env;
|
||||||
_filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
|
var dataRoot = Path.Combine(Directory.GetCurrentDirectory(), "data"); // /app/data i containern
|
||||||
|
_filePath = Path.Combine(dataRoot, "infrastructure", "setup.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +58,10 @@ namespace Aberwyn.Data
|
|||||||
{
|
{
|
||||||
public static SetupSettings Load(IHostEnvironment env)
|
public static SetupSettings Load(IHostEnvironment env)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(env.ContentRootPath, "infrastructure", "setup.json");
|
var dataRoot = Path.Combine(Directory.GetCurrentDirectory(), "data"); // /app/data i containern
|
||||||
var json = File.ReadAllText(path);
|
var filePath = Path.Combine(dataRoot, "infrastructure", "setup.json");
|
||||||
|
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
return JsonSerializer.Deserialize<SetupSettings>(json)!;
|
return JsonSerializer.Deserialize<SetupSettings>(json)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
Aberwyn/Data/TorrentRssService.cs
Normal file
41
Aberwyn/Data/TorrentRssService.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
|
||||||
|
namespace Aberwyn.Services
|
||||||
|
{
|
||||||
|
public class TorrentRssService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<TorrentRssService> _logger;
|
||||||
|
|
||||||
|
public TorrentRssService(IServiceProvider serviceProvider, ILogger<TorrentRssService> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
// Vänta lite innan första körningen
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var rssProcessor = scope.ServiceProvider.GetRequiredService<IRssProcessor>();
|
||||||
|
await rssProcessor.ProcessRssFeeds();
|
||||||
|
|
||||||
|
_logger.LogInformation("RSS feeds processed successfully at {Time}", DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing RSS feeds");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vänta 10 minuter innan nästa körning
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using BencodeNET.Objects;
|
||||||
using BencodeNET.Parsing;
|
using BencodeNET.Parsing;
|
||||||
using BencodeNET.Torrents;
|
using BencodeNET.Torrents;
|
||||||
using BencodeNET.Objects;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
public interface ITorrentService
|
public interface ITorrentService
|
||||||
{
|
{
|
||||||
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
|
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
|
||||||
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
|
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
|
||||||
|
Task<List<TorrentItem>> GetRecentTorrentsAsync(int count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TorrentService : ITorrentService
|
public class TorrentService : ITorrentService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<TorrentService> _logger;
|
private readonly ILogger<TorrentService> _logger;
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
// Kända trackers och deras egenskaper
|
// Kända trackers och deras egenskaper
|
||||||
private readonly Dictionary<string, TrackerInfo> _knownTrackers = new()
|
private readonly Dictionary<string, TrackerInfo> _knownTrackers = new()
|
||||||
@@ -27,13 +31,21 @@ public class TorrentService : ITorrentService
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public TorrentService(HttpClient httpClient, ILogger<TorrentService> logger)
|
public TorrentService(HttpClient httpClient, ILogger<TorrentService> logger, ApplicationDbContext context)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_context = context;
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<TorrentItem>> GetRecentTorrentsAsync(int count)
|
||||||
|
{
|
||||||
|
return await _context.TorrentItems
|
||||||
|
.OrderByDescending(t => t.PublishDate)
|
||||||
|
.Take(count)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
public async Task<TorrentInfo> ParseTorrentAsync(IFormFile file)
|
public async Task<TorrentInfo> ParseTorrentAsync(IFormFile file)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -87,17 +99,31 @@ public class TorrentService : ITorrentService
|
|||||||
|
|
||||||
if (bdict.TryGetValue("files", out var filesValue) && filesValue is BDictionary files)
|
if (bdict.TryGetValue("files", out var filesValue) && filesValue is BDictionary files)
|
||||||
{
|
{
|
||||||
if (TryGetStatsFromFiles(files, info.InfoHash, info) ||
|
// Använd direkt byte array istället för att konvertera till sträng
|
||||||
TryGetStatsFromFiles(files, Encoding.UTF8.GetString(info.InfoHashBytes), info))
|
if (TryGetStatsFromFiles(files, info.InfoHashBytes, info))
|
||||||
{
|
{
|
||||||
info.HasTrackerData = true;
|
info.HasTrackerData = true;
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
|
|
||||||
}
|
// Om det inte fungerar, prova att URL-decode först
|
||||||
else
|
if (!string.IsNullOrEmpty(info.InfoHash))
|
||||||
{
|
{
|
||||||
info.ErrorMessage = "Inget 'files' objekt i tracker-svaret";
|
try
|
||||||
|
{
|
||||||
|
string decoded = Uri.UnescapeDataString(info.InfoHash);
|
||||||
|
byte[] decodedBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(decoded);
|
||||||
|
|
||||||
|
if (TryGetStatsFromFiles(files, decodedBytes, info))
|
||||||
|
{
|
||||||
|
info.HasTrackerData = true;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Ignore decode errors */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
@@ -118,10 +144,21 @@ public class TorrentService : ITorrentService
|
|||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
private bool ByteArraysEqual(byte[] a, byte[] b)
|
||||||
private bool TryGetStatsFromFiles(BDictionary files, string hashKey, TorrentInfo info)
|
|
||||||
{
|
{
|
||||||
if (files.TryGetValue(hashKey, out var hashEntry) && hashEntry is BDictionary stats)
|
if (a.Length != b.Length) return false;
|
||||||
|
for (int i = 0; i < a.Length; i++)
|
||||||
|
{
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private bool TryGetStatsFromFiles(BDictionary files, byte[] hashBytes, TorrentInfo info)
|
||||||
|
{
|
||||||
|
// Skapa en BString från byte array
|
||||||
|
var bStringKey = new BString(hashBytes);
|
||||||
|
|
||||||
|
if (files.TryGetValue(bStringKey, out var hashEntry) && hashEntry is BDictionary stats)
|
||||||
{
|
{
|
||||||
info.Seeders = stats.TryGetInt("complete") ?? 0;
|
info.Seeders = stats.TryGetInt("complete") ?? 0;
|
||||||
info.Leechers = stats.TryGetInt("incomplete") ?? 0;
|
info.Leechers = stats.TryGetInt("incomplete") ?? 0;
|
||||||
@@ -130,6 +167,23 @@ public class TorrentService : ITorrentService
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
public async Task<int> GetUnseenTorrentCountAsync(string userId)
|
||||||
|
{
|
||||||
|
// Hämta alla infohashes som användaren redan sett
|
||||||
|
var seenHashes = await _context.UserTorrentSeen
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => x.InfoHash)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Räkna alla torrents som inte finns i seenHashes och som har > 40 seeders
|
||||||
|
var count = await _context.TorrentItems
|
||||||
|
.Where(t => t.InfoHash != null
|
||||||
|
&& !seenHashes.Contains(t.InfoHash)
|
||||||
|
&& t.Seeders > 40)
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
private string ConvertAnnounceToScrape(string announceUrl)
|
private string ConvertAnnounceToScrape(string announceUrl)
|
||||||
{
|
{
|
||||||
|
|||||||
11
Aberwyn/Data/infrastructure/setup.json
Normal file
11
Aberwyn/Data/infrastructure/setup.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AdminUsername": "admin",
|
||||||
|
"AdminEmail": "admin@localhost",
|
||||||
|
"AdminPassword": "Admin123!",
|
||||||
|
"IsConfigured": true,
|
||||||
|
"DbHost": "192.168.1.108",
|
||||||
|
"DbPort": 3306,
|
||||||
|
"DbName": "lewel_prod",
|
||||||
|
"DbUser": "lewel",
|
||||||
|
"DbPassword": "W542.Hl;)%ta"
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ WORKDIR /app
|
|||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
|
VOLUME /app/data
|
||||||
|
|
||||||
# Byggimage med SDK
|
# Byggimage med SDK
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|||||||
1177
Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs
generated
Normal file
1177
Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
105
Aberwyn/Migrations/20250805202224_AddTorrentTables.cs
Normal file
105
Aberwyn/Migrations/20250805202224_AddTorrentTables.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddTorrentTables : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DownloadRules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
KeywordFilter = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CategoryFilter = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
MinSeeders = table.Column<int>(type: "int", nullable: false),
|
||||||
|
MaxSize = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
AutoDownload = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DownloadRules", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RssFeeds",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Url = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
LastChecked = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RssFeeds", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TorrentItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Title = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
MagnetLink = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
InfoHash = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
PublishDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
Size = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Seeders = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Leechers = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Category = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
RssSource = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Description = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
TorrentUrl = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TorrentItems", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TorrentItems_InfoHash",
|
||||||
|
table: "TorrentItems",
|
||||||
|
column: "InfoHash",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DownloadRules");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RssFeeds");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TorrentItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1195
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs
generated
Normal file
1195
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs
Normal file
69
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddTorrentTablesv2 : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Completed",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DownloadKey",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "MovieName",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Token",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Year",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Completed",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DownloadKey",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MovieName",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Token",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Year",
|
||||||
|
table: "TorrentItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1188
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs
generated
Normal file
1188
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
211
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs
Normal file
211
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class MakeTorrentFieldsNullable : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "TorrentUrl",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Token",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "MovieName",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "MagnetLink",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "InfoHash",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "varchar(255)",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(255)")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DownloadKey",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Category",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "TorrentUrl",
|
||||||
|
keyValue: null,
|
||||||
|
column: "TorrentUrl",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "TorrentUrl",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "Token",
|
||||||
|
keyValue: null,
|
||||||
|
column: "Token",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Token",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "MovieName",
|
||||||
|
keyValue: null,
|
||||||
|
column: "MovieName",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "MovieName",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "MagnetLink",
|
||||||
|
keyValue: null,
|
||||||
|
column: "MagnetLink",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "MagnetLink",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "InfoHash",
|
||||||
|
keyValue: null,
|
||||||
|
column: "InfoHash",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "InfoHash",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "varchar(255)",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(255)",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "DownloadKey",
|
||||||
|
keyValue: null,
|
||||||
|
column: "DownloadKey",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DownloadKey",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "TorrentItems",
|
||||||
|
keyColumn: "Category",
|
||||||
|
keyValue: null,
|
||||||
|
column: "Category",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Category",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1237
Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs
generated
Normal file
1237
Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddMovieMetadataToTorrentItem : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Actors",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Director",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Genre",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_ImdbID",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_ImdbRating",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Plot",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Poster",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Runtime",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Title",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Year",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Actors",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Director",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Genre",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_ImdbID",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_ImdbRating",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Plot",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Poster",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Runtime",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Title",
|
||||||
|
table: "TorrentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Year",
|
||||||
|
table: "TorrentItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1240
Aberwyn/Migrations/20250820133435_AddProvidersToMovieMetadata.Designer.cs
generated
Normal file
1240
Aberwyn/Migrations/20250820133435_AddProvidersToMovieMetadata.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddProvidersToMovieMetadata : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Metadata_Providers",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata_Providers",
|
||||||
|
table: "TorrentItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1261
Aberwyn/Migrations/20250824172213_AddUserTorrentSeen.Designer.cs
generated
Normal file
1261
Aberwyn/Migrations/20250824172213_AddUserTorrentSeen.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
Aberwyn/Migrations/20250824172213_AddUserTorrentSeen.cs
Normal file
37
Aberwyn/Migrations/20250824172213_AddUserTorrentSeen.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddUserTorrentSeen : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserTorrentSeen",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
UserId = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
TorrentId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SeenDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserTorrentSeen", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserTorrentSeen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1262
Aberwyn/Migrations/20250824172938_AddUserTorrentSeenv2.Designer.cs
generated
Normal file
1262
Aberwyn/Migrations/20250824172938_AddUserTorrentSeenv2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
Aberwyn/Migrations/20250824172938_AddUserTorrentSeenv2.cs
Normal file
37
Aberwyn/Migrations/20250824172938_AddUserTorrentSeenv2.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddUserTorrentSeenv2 : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TorrentId",
|
||||||
|
table: "UserTorrentSeen");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InfoHash",
|
||||||
|
table: "UserTorrentSeen",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InfoHash",
|
||||||
|
table: "UserTorrentSeen");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "TorrentId",
|
||||||
|
table: "UserTorrentSeen",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1304
Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs
generated
Normal file
1304
Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
Aberwyn/Migrations/20250903130637_AddDoughPlans.cs
Normal file
44
Aberwyn/Migrations/20250903130637_AddDoughPlans.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddDoughPlans : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DoughPlans",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
AntalPizzor = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ViktPerPizza = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Mjol = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Vatten = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Olja = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Salt = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Jast = table.Column<double>(type: "double", nullable: false),
|
||||||
|
TotalDeg = table.Column<double>(type: "double", nullable: false),
|
||||||
|
Datum = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
Namn = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DoughPlans", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DoughPlans");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1349
Aberwyn/Migrations/20250921133610_AddMealWish.Designer.cs
generated
Normal file
1349
Aberwyn/Migrations/20250921133610_AddMealWish.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
Aberwyn/Migrations/20250921133610_AddMealWish.cs
Normal file
53
Aberwyn/Migrations/20250921133610_AddMealWish.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddMealWish : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MealWishes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Recipe = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
LinkedMealId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
RequestedByUserId = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
IsArchived = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsImported = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MealWishes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MealWishes_Meals_LinkedMealId",
|
||||||
|
column: x => x.LinkedMealId,
|
||||||
|
principalTable: "Meals",
|
||||||
|
principalColumn: "Id");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MealWishes_LinkedMealId",
|
||||||
|
table: "MealWishes",
|
||||||
|
column: "LinkedMealId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MealWishes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1359
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.Designer.cs
generated
Normal file
1359
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.cs
Normal file
56
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddMealWishUserRelation : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "RequestedByUserId",
|
||||||
|
table: "MealWishes",
|
||||||
|
type: "varchar(255)",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MealWishes_RequestedByUserId",
|
||||||
|
table: "MealWishes",
|
||||||
|
column: "RequestedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_MealWishes_AspNetUsers_RequestedByUserId",
|
||||||
|
table: "MealWishes",
|
||||||
|
column: "RequestedByUserId",
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_MealWishes_AspNetUsers_RequestedByUserId",
|
||||||
|
table: "MealWishes");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_MealWishes_RequestedByUserId",
|
||||||
|
table: "MealWishes");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "RequestedByUserId",
|
||||||
|
table: "MealWishes",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(255)")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1362
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.Designer.cs
generated
Normal file
1362
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.cs
Normal file
27
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddDateToWeeklyMenu : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "Date",
|
||||||
|
table: "WeeklyMenu",
|
||||||
|
type: "datetime(6)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Date",
|
||||||
|
table: "WeeklyMenu");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1365
Aberwyn/Migrations/20251027205014_AddTorrentItemTable.Designer.cs
generated
Normal file
1365
Aberwyn/Migrations/20251027205014_AddTorrentItemTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
Aberwyn/Migrations/20251027205014_AddTorrentItemTable.cs
Normal file
26
Aberwyn/Migrations/20251027205014_AddTorrentItemTable.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddTorrentItemTable : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsDownloaded",
|
||||||
|
table: "TorrentItems",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsDownloaded",
|
||||||
|
table: "TorrentItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,48 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("BudgetPeriods");
|
b.ToTable("BudgetPeriods");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.DoughPlan", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("AntalPizzor")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Datum")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<double>("Jast")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<double>("Mjol")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<string>("Namn")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<double>("Olja")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<double>("Salt")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<double>("TotalDeg")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<double>("Vatten")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.Property<double>("ViktPerPizza")
|
||||||
|
.HasColumnType("double");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DoughPlans");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
|
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -442,6 +484,44 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("MealRatings");
|
b.ToTable("MealRatings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.MealWish", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsArchived")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsImported")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int?>("LinkedMealId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Recipe")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("RequestedByUserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LinkedMealId");
|
||||||
|
|
||||||
|
b.HasIndex("RequestedByUserId");
|
||||||
|
|
||||||
|
b.ToTable("MealWishes");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
|
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -677,6 +757,9 @@ namespace Aberwyn.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<int>("DayOfWeek")
|
b.Property<int>("DayOfWeek")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -697,6 +780,34 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("WeeklyMenu", (string)null);
|
b.ToTable("WeeklyMenu", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DownloadRule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoDownload")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("CategoryFilter")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("KeywordFilter")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<long>("MaxSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("MinSeeders")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DownloadRules");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -825,6 +936,128 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("AspNetUserTokens", (string)null);
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RssFeed", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastChecked")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("RssFeeds");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TorrentItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Completed")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("DownloadKey")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("InfoHash")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDownloaded")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("Leechers")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("MagnetLink")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("MovieName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PublishDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("RssSource")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Seeders")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<long>("Size")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("TorrentUrl")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int?>("Year")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InfoHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TorrentItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UserTorrentSeen", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("InfoHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SeenDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("UserTorrentSeen");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
||||||
@@ -908,6 +1141,23 @@ namespace Aberwyn.Migrations
|
|||||||
b.Navigation("Meal");
|
b.Navigation("Meal");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.MealWish", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.Meal", "LinkedMeal")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LinkedMealId");
|
||||||
|
|
||||||
|
b.HasOne("Aberwyn.Models.ApplicationUser", "RequestedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RequestedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("LinkedMeal");
|
||||||
|
|
||||||
|
b.Navigation("RequestedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
|
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder")
|
b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder")
|
||||||
@@ -1018,6 +1268,58 @@ namespace Aberwyn.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TorrentItem", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("MovieMetadata", "Metadata", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("TorrentItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b1.Property<string>("Actors")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Director")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Genre")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("ImdbID")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("ImdbRating")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Plot")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Poster")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Providers")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Runtime")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.Property<string>("Year")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b1.HasKey("TorrentItemId");
|
||||||
|
|
||||||
|
b1.ToTable("TorrentItems");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("TorrentItemId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
|
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Preferences")
|
b.Navigation("Preferences")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace Aberwyn.Models
|
|||||||
public List<WeeklyMenu> WeeklyMenus { get; set; } // List of weekly menu entries
|
public List<WeeklyMenu> WeeklyMenus { get; set; } // List of weekly menu entries
|
||||||
public int WeekNumber { get; set; } // Week number for the menu
|
public int WeekNumber { get; set; } // Week number for the menu
|
||||||
public int Year { get; set; } // Year for the menu
|
public int Year { get; set; } // Year for the menu
|
||||||
|
|
||||||
}
|
}
|
||||||
public class WeeklyMenuDto
|
public class WeeklyMenuDto
|
||||||
{
|
{
|
||||||
@@ -40,6 +41,7 @@ public class WeeklyMenu
|
|||||||
public int? LunchMealId { get; set; }
|
public int? LunchMealId { get; set; }
|
||||||
public int? DinnerMealId { get; set; }
|
public int? DinnerMealId { get; set; }
|
||||||
public string? Cook { get; set; }
|
public string? Cook { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
public int WeekNumber { get; set; }
|
public int WeekNumber { get; set; }
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
@@ -59,6 +61,8 @@ public class WeeklyMenu
|
|||||||
public class RecentMenuEntry
|
public class RecentMenuEntry
|
||||||
{
|
{
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
|
public int WeekNumber { get; set; } // Lägg till vecka
|
||||||
|
public int Year { get; set; } // Lägg till år
|
||||||
public string BreakfastMealName { get; set; }
|
public string BreakfastMealName { get; set; }
|
||||||
public string LunchMealName { get; set; }
|
public string LunchMealName { get; set; }
|
||||||
public string DinnerMealName { get; set; }
|
public string DinnerMealName { get; set; }
|
||||||
@@ -176,6 +180,51 @@ public class WeeklyMenu
|
|||||||
|
|
||||||
public List<Meal> Meals { get; set; } = new();
|
public List<Meal> Meals { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
public class MealWish
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Recipe { get; set; }
|
||||||
|
public int? LinkedMealId { get; set; }
|
||||||
|
[ForeignKey("LinkedMealId")]
|
||||||
|
public Meal? LinkedMeal { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string RequestedByUserId { get; set; }
|
||||||
|
[ForeignKey(nameof(RequestedByUserId))]
|
||||||
|
public ApplicationUser? RequestedByUser { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool IsArchived { get; set; } = false;
|
||||||
|
public bool IsImported { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class CreateMealWishDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Recipe { get; set; }
|
||||||
|
}
|
||||||
|
public class MealWishDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string? Recipe { get; set; }
|
||||||
|
public bool IsArchived { get; set; }
|
||||||
|
public bool IsImported { get; set; }
|
||||||
|
|
||||||
|
public static MealWishDto FromEntity(MealWish wish)
|
||||||
|
{
|
||||||
|
return new MealWishDto
|
||||||
|
{
|
||||||
|
Id = wish.Id,
|
||||||
|
Name = wish.Name,
|
||||||
|
Recipe = wish.Recipe,
|
||||||
|
IsArchived = wish.IsArchived,
|
||||||
|
IsImported = wish.IsImported
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class MealRating
|
public class MealRating
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,5 +26,22 @@ namespace Aberwyn.Models
|
|||||||
public bool RestaurantIsOpen { get; set; }
|
public bool RestaurantIsOpen { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DoughPlan
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int AntalPizzor { get; set; }
|
||||||
|
public double ViktPerPizza { get; set; }
|
||||||
|
|
||||||
|
public double Mjol { get; set; }
|
||||||
|
public double Vatten { get; set; }
|
||||||
|
public double Olja { get; set; }
|
||||||
|
public double Salt { get; set; }
|
||||||
|
public double Jast { get; set; }
|
||||||
|
public double TotalDeg { get; set; }
|
||||||
|
|
||||||
|
public DateTime Datum { get; set; }
|
||||||
|
public string Namn { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
namespace Aberwyn.Models
|
namespace Aberwyn.Models
|
||||||
{
|
{
|
||||||
public class BudgetReportRequestDto
|
public class BudgetReportViewModel
|
||||||
{
|
{
|
||||||
public List<int> DefinitionIds { get; set; } = new();
|
public int? Year { get; set; }
|
||||||
public int StartYear { get; set; }
|
public string GroupBy { get; set; } = "month";
|
||||||
public int StartMonth { get; set; }
|
public string? ItemLabel { get; set; }
|
||||||
public int EndYear { get; set; }
|
public string? CategoryLabel { get; set; }
|
||||||
public int EndMonth { get; set; }
|
public List<int> ItemDefinitionIds { get; set; } = new();
|
||||||
|
public List<int> CategoryDefinitionIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BudgetReportResultDto
|
|
||||||
{
|
|
||||||
public int Year { get; set; }
|
|
||||||
public int Month { get; set; }
|
|
||||||
public List<DefinitionSumDto> Definitions { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DefinitionSumDto
|
|
||||||
{
|
|
||||||
public int DefinitionId { get; set; }
|
|
||||||
public string DefinitionName { get; set; }
|
|
||||||
public decimal TotalAmount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
public class TorrentInfo
|
public class TorrentInfo
|
||||||
{
|
{
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
@@ -11,6 +13,7 @@ public class TorrentInfo
|
|||||||
public int Completed { get; set; } = 0;
|
public int Completed { get; set; } = 0;
|
||||||
public bool HasTrackerData { get; set; } = false;
|
public bool HasTrackerData { get; set; } = false;
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class TrackerInfo
|
public class TrackerInfo
|
||||||
{
|
{
|
||||||
@@ -27,3 +30,155 @@ public class TorrentUploadViewModel
|
|||||||
public TorrentInfo TorrentInfo { get; set; }
|
public TorrentInfo TorrentInfo { get; set; }
|
||||||
public bool ShowResults { get; set; } = false;
|
public bool ShowResults { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RssFeed
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public DateTime LastChecked { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
|
public class TorrentItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? MagnetLink { get; set; }
|
||||||
|
public string? InfoHash { get; set; }
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
public long Size { get; set; } = 0;
|
||||||
|
public int Seeders { get; set; } = 0;
|
||||||
|
public int Leechers { get; set; } = 0;
|
||||||
|
public int Completed { get; set; } = 0;
|
||||||
|
public TorrentStatus Status { get; set; } = TorrentStatus.New;
|
||||||
|
public string? Category { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string RssSource { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? TorrentUrl { get; set; }
|
||||||
|
public string? MovieName { get; set; }
|
||||||
|
public int? Year { get; set; }
|
||||||
|
public string? DownloadKey { get; set; }
|
||||||
|
public string? Token { get; set; }
|
||||||
|
|
||||||
|
public MovieMetadata? Metadata { get; set; }
|
||||||
|
public bool IsDownloaded { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MovieMetadata
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty; // required
|
||||||
|
public string? Year { get; set; }
|
||||||
|
public string? Poster { get; set; }
|
||||||
|
public string? ImdbRating { get; set; }
|
||||||
|
public string? Genre { get; set; }
|
||||||
|
public string? Plot { get; set; }
|
||||||
|
public string? Director { get; set; }
|
||||||
|
public string? Actors { get; set; }
|
||||||
|
public string? Runtime { get; set; }
|
||||||
|
public string? ImdbID { get; set; }
|
||||||
|
public string? Providers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TorrentListViewModel
|
||||||
|
{
|
||||||
|
public List<TorrentListItemViewModel> Items { get; set; } = new();
|
||||||
|
public int CurrentPage { get; set; }
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
public string CurrentSort { get; set; } = "date";
|
||||||
|
public string CurrentRange { get; set; } = "all";
|
||||||
|
public string CurrentPeriod { get; set; } = "all"; // day/week/month/all
|
||||||
|
}
|
||||||
|
public class TorrentListItemViewModel
|
||||||
|
{
|
||||||
|
public string InfoHash { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string MovieName { get; set; } = string.Empty;
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
public int Seeders { get; set; }
|
||||||
|
public int Leechers { get; set; }
|
||||||
|
public string? TorrentUrl { get; set; }
|
||||||
|
public MovieMetadata? Metadata { get; set; }
|
||||||
|
public bool IsNew { get; set; } = false;
|
||||||
|
public List<string> AvailableOn { get; set; } = new();
|
||||||
|
public bool IsDownloaded { get; set; }
|
||||||
|
}
|
||||||
|
public class JustWatchResponse
|
||||||
|
{
|
||||||
|
public Data data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
public SearchTitles searchTitles { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SearchTitles
|
||||||
|
{
|
||||||
|
public List<Edge> edges { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Edge
|
||||||
|
{
|
||||||
|
public Node node { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string title { get; set; }
|
||||||
|
public int? releaseYear { get; set; }
|
||||||
|
public Content content { get; set; }
|
||||||
|
public List<Offer> offers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Content
|
||||||
|
{
|
||||||
|
public string imdbId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Offer
|
||||||
|
{
|
||||||
|
public Provider provider { get; set; }
|
||||||
|
public string monetizationType { get; set; } // FLATRATE, RENT, BUY
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Provider
|
||||||
|
{
|
||||||
|
public int id { get; set; }
|
||||||
|
public string clearName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TorrentStatus
|
||||||
|
{
|
||||||
|
New,
|
||||||
|
Downloaded,
|
||||||
|
Processing,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Ignored
|
||||||
|
}
|
||||||
|
public class DownloadRule
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string KeywordFilter { get; set; }
|
||||||
|
public string CategoryFilter { get; set; }
|
||||||
|
public int MinSeeders { get; set; }
|
||||||
|
public long MaxSize { get; set; }
|
||||||
|
public bool AutoDownload { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserTorrentSeen
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserId { get; set; } = null!;
|
||||||
|
public string InfoHash { get; set; } = null!; // unikt för torrent
|
||||||
|
public DateTime SeenDate { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ namespace Aberwyn.Models
|
|||||||
public List<RecentMenuEntry> RecentEntries { get; set; } = new();
|
public List<RecentMenuEntry> RecentEntries { get; set; } = new();
|
||||||
public List<WeeklyMenu> WeeklyMenus { get; set; } = new();
|
public List<WeeklyMenu> WeeklyMenus { get; set; } = new();
|
||||||
public List<UserModel> AvailableCooks { get; set; } = new();
|
public List<UserModel> AvailableCooks { get; set; } = new();
|
||||||
|
public List<MealWish> WishList { get; set; } = new();
|
||||||
|
|
||||||
public List<WeeklyMenuViewModel> PreviousWeeks { get; set; } = new();
|
public List<WeeklyMenuViewModel> PreviousWeeks { get; set; } = new();
|
||||||
public class RecentMenuEntry
|
public class RecentMenuEntry
|
||||||
@@ -37,6 +38,8 @@ namespace Aberwyn.Models
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using Aberwyn.Services;
|
|||||||
|
|
||||||
var config = new ConfigurationBuilder()
|
var config = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
.AddJsonFile("appsettings.json", optional: false)
|
.AddJsonFile("appsettings.json", optional: true)
|
||||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
@@ -25,7 +25,9 @@ builder.Configuration.AddConfiguration(config);
|
|||||||
|
|
||||||
|
|
||||||
// Läser setup.json eller skapar en ny tom om den inte finns
|
// Läser setup.json eller skapar en ny tom om den inte finns
|
||||||
var setupFilePath = Path.Combine("infrastructure", "setup.json");
|
var dataRoot = Path.Combine(Directory.GetCurrentDirectory(), "data");
|
||||||
|
var setupFilePath = Path.Combine(dataRoot, "infrastructure", "setup.json");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(setupFilePath)!);
|
||||||
|
|
||||||
if (!File.Exists(setupFilePath))
|
if (!File.Exists(setupFilePath))
|
||||||
{
|
{
|
||||||
@@ -59,13 +61,24 @@ try
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"❌ Fel vid läsning av setup.json: {ex.Message}");
|
Console.WriteLine($"Fel vid läsning av setup.json: {ex.Message}");
|
||||||
setup = new SetupSettings { IsConfigured = false };
|
setup = new SetupSettings { IsConfigured = false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
|
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
|
||||||
builder.Services.AddScoped<ITorrentService, TorrentService>();
|
builder.Services.AddScoped<ITorrentService, TorrentService>();
|
||||||
|
builder.Services.AddHttpClient<RssProcessor>();
|
||||||
|
builder.Services.AddScoped<IRssProcessor, RssProcessor>();
|
||||||
|
builder.Services.AddHostedService<TorrentRssService>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<HdTorrentsTrackerScraper>();
|
||||||
|
builder.Services.AddScoped<HdTorrentsTrackerScraper>();
|
||||||
|
builder.Services.AddHttpClient<DelugeClient>();
|
||||||
|
builder.Services.AddHttpClient<MovieMetadataService>();
|
||||||
|
builder.Services.AddScoped<ITorrentService, TorrentService>();
|
||||||
|
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
|
||||||
|
builder.Services.AddScoped<TorrentService>();
|
||||||
|
|
||||||
// Add services to the container
|
// Add services to the container
|
||||||
builder.Services.AddControllersWithViews()
|
builder.Services.AddControllersWithViews()
|
||||||
@@ -166,7 +179,24 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
|
|||||||
options.SupportedUICultures = supportedCultures;
|
options.SupportedUICultures = supportedCultures;
|
||||||
});
|
});
|
||||||
builder.Services.AddSingleton<SetupService>();
|
builder.Services.AddSingleton<SetupService>();
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.AddConsole();
|
||||||
|
builder.Logging.AddDebug();
|
||||||
|
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin() // Tillåt alla domäner
|
||||||
|
.AllowAnyHeader() // Tillåt alla headers
|
||||||
|
.AllowAnyMethod(); // Tillåt GET, POST, etc.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Eller om du vill ha mer detaljerad loggning:
|
||||||
|
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
@@ -197,7 +227,7 @@ app.UseRouting();
|
|||||||
app.UseSession();
|
app.UseSession();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
app.UseCors("AllowAll");
|
||||||
// Routing
|
// Routing
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
|
|||||||
70
Aberwyn/Views/Budget/Elkostnad.cshtml
Normal file
70
Aberwyn/Views/Budget/Elkostnad.cshtml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Elkostnad";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Räkna ut elkostnad</h2>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="spotpris">Månadens spotpris (öre/kWh):</label>
|
||||||
|
<input type="number" id="spotpris" class="form-control" step="0.01" value="52.33" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="forbrukning">Förbrukning (kWh):</label>
|
||||||
|
<input type="number" id="forbrukning" class="form-control" value="350" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary mt-2" onclick="berakna()">Beräkna</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="resultat" class="alert alert-info mt-3" style="display:none;"></div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
function berakna() {
|
||||||
|
// Inputs
|
||||||
|
let spotprisOre = parseFloat(document.getElementById("spotpris").value) || 0;
|
||||||
|
let spotprisKr = spotprisOre / 100; // omvandling öre → kr
|
||||||
|
let forbrukning = parseFloat(document.getElementById("forbrukning").value) || 0;
|
||||||
|
|
||||||
|
// Elhandel – fasta tillägg (kr/kWh)
|
||||||
|
const paslag = 0.029;
|
||||||
|
const rorliga = 0.0567;
|
||||||
|
const ursprung = 0.016;
|
||||||
|
|
||||||
|
// Elnät – fasta avgifter (kr/kWh)
|
||||||
|
const eloverforing = 0.29;
|
||||||
|
const energiskatt = 0.439;
|
||||||
|
|
||||||
|
// Fasta avgifter per månad (kr)
|
||||||
|
const elhandelFast = 39.20;
|
||||||
|
const elnatFast = 335.00;
|
||||||
|
const fastSumma = elhandelFast + elnatFast;
|
||||||
|
|
||||||
|
// Rörliga priser
|
||||||
|
let elhandelPerKwh = spotprisKr + paslag + rorliga + ursprung;
|
||||||
|
let elnatPerKwh = eloverforing + energiskatt;
|
||||||
|
let totaltPerKwh = elhandelPerKwh + elnatPerKwh;
|
||||||
|
|
||||||
|
// Kostnader
|
||||||
|
let rorligt = forbrukning * totaltPerKwh;
|
||||||
|
let totalExMoms = rorligt + fastSumma;
|
||||||
|
let moms = totalExMoms * 0.25;
|
||||||
|
let totalInklMoms = totalExMoms + moms;
|
||||||
|
|
||||||
|
// Resultat
|
||||||
|
let resultat = document.getElementById("resultat");
|
||||||
|
resultat.style.display = "block";
|
||||||
|
resultat.innerHTML = `
|
||||||
|
<h4>Resultat</h4>
|
||||||
|
<p>Rörligt (Elhandel + Elnät): ${rorligt.toFixed(2)} kr</p>
|
||||||
|
<p>Fasta avgifter: ${fastSumma.toFixed(2)} kr</p>
|
||||||
|
<p>Moms (25%): ${moms.toFixed(2)} kr</p>
|
||||||
|
<hr />
|
||||||
|
<strong>Total kostnad: ${totalInklMoms.toFixed(2)} kr</strong><br />
|
||||||
|
<small>Snittpris: ${(totalInklMoms / forbrukning).toFixed(2)} kr/kWh</small>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
|
|
||||||
<div ng-app="budgetApp" ng-controller="BudgetController">
|
<div ng-app="budgetApp" ng-controller="BudgetController">
|
||||||
<div class="budget-header" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
|
<div class="budget-header" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
|
||||||
<div class="month-nav-bar" style="display: flex; align-items: center; gap: 10px; position: relative;">
|
|
||||||
|
<div class="month-nav-bar" ng-if="!budget.name" style="display: flex; align-items: center; gap: 10px; position: relative;">
|
||||||
|
<a href="/budget/list" class="nav-button" style="margin-right: 10px;">
|
||||||
|
Lista
|
||||||
|
</a>
|
||||||
<button class="nav-button" ng-click="previousMonth()">←</button>
|
<button class="nav-button" ng-click="previousMonth()">←</button>
|
||||||
<span class="month-label" ng-click="showMonthPicker = !showMonthPicker" style="cursor: pointer;">
|
<span class="month-label" ng-click="showMonthPicker = !showMonthPicker" style="cursor: pointer;">
|
||||||
{{ selectedMonthName }} {{ selectedYear }}
|
{{ selectedMonthName }} {{ selectedYear }}
|
||||||
@@ -20,16 +24,24 @@
|
|||||||
<button class="nav-button" ng-click="applyMonthSelection()">Välj</button>
|
<button class="nav-button" ng-click="applyMonthSelection()">Välj</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="month-nav-bar" ng-if="budget.name">
|
||||||
|
<a href="/budget/list" class="nav-button" style="margin-right: 10px;">
|
||||||
|
Lista
|
||||||
|
</a>
|
||||||
|
<span class="month-label" ng-bind="budget.name"></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="menu-container" ng-class="{ 'open': menuOpen }">
|
<div class="menu-container" ng-class="{ 'open': menuOpen }">
|
||||||
<button class="icon-button" ng-click="toggleMenu($event)">
|
<button class="icon-button" ng-click="toggleMenu($event)">
|
||||||
<i class="fa fa-ellipsis-v"></i>
|
<i class="fa fa-ellipsis-v"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu" ng-show="menuOpen">
|
<div class="dropdown-menu" ng-show="menuOpen">
|
||||||
<button ng-click="copyPreviousMonthSafe()">Kopiera föregående månad</button>
|
<button class="nav-button" ng-click="createNamedBudget()">Ny budget</button>
|
||||||
<button ng-click="deleteMonth(); menuOpen = false;" class="danger">Ta bort hela månaden</button>
|
<button ng-click="copyBudget(); menuOpen = false;">Kopiera</button>
|
||||||
|
<button ng-click="deleteBudget(); menuOpen = false;" class="danger">Ta bort</button>
|
||||||
<button ng-click="createNewCategory(); menuOpen = false;">Lägg till ny kategori</button>
|
<button ng-click="createNewCategory(); menuOpen = false;">Lägg till ny kategori</button>
|
||||||
<!--<button ng-click="openImportModule(); menuOpen = false;">📥 Importera rader</button> -->
|
<button ng-click="openImportModule(); menuOpen = false;">Importera text</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,6 +150,10 @@
|
|||||||
|
|
||||||
<input type="text" class="item-label" ng-model="item.name" ng-if="cat.editing" />
|
<input type="text" class="item-label" ng-model="item.name" ng-if="cat.editing" />
|
||||||
<span ng-if="!cat.editing" class="item-label" title="{{ item.name }}">{{ item.name }}</span>
|
<span ng-if="!cat.editing" class="item-label" title="{{ item.name }}">{{ item.name }}</span>
|
||||||
|
<!-- debug
|
||||||
|
<span ng-if="!cat.editing" class="item-definition" title="{{ item.definitionName }}">#{{ item.budgetItemDefinitionId }} {{ item.definitionName }}</span>
|
||||||
|
<input type="text" ng-model="item.budgetItemDefinitionId" ng-if="cat.editing" />
|
||||||
|
-->
|
||||||
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
|
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
|
||||||
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
|
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
|
||||||
|
|
||||||
@@ -192,20 +208,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="no-data" ng-if="!loading && budget && budget.categories.length === 0">
|
|
||||||
<p>Det finns ingen budgetdata för
|
<!-- Ingen budget alls -->
|
||||||
<strong>{{ budget.name || (selectedMonth + '/' + selectedYear) }}</strong>.
|
<div class="no-data" ng-if="!loading && (!budget || !budget.id)">
|
||||||
|
<p>
|
||||||
|
<strong ng-bind="budget && budget.name
|
||||||
|
? 'Budgeten \" ' + budget.name + ' \" finns inte.'
|
||||||
|
: 'Det finns ingen budget för ' + getMonthName(selectedMonth) + ' ' + selectedYear + '.' ">
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
|
<!-- Skapa ny budget alltid om budget saknas -->
|
||||||
<button ng-click="createEmptyBudget()" style="margin-right: 10px;">
|
<button ng-click="createEmptyBudget()" style="margin-right: 10px;">
|
||||||
Skapa ny budget
|
Skapa ny budget
|
||||||
</button>
|
</button>
|
||||||
<button ng-click="copyPreviousMonthSafe()">
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Budget finns men inga kategorier -->
|
||||||
|
<div class="no-data" ng-if="!loading && budget && budget.id && (!budget.categories || budget.categories.length === 0)">
|
||||||
|
<p>
|
||||||
|
Budgeten <strong>{{ budget.name || (getMonthName(selectedMonth) + " " + selectedYear) }}</strong> har inga kategorier än.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<button ng-click="createNewCategory()" style="margin-right: 10px;">
|
||||||
|
Skapa ny kategori
|
||||||
|
</button>
|
||||||
|
<button ng-if="!budget.name" ng-click="copyPreviousMonthSafe()" style="margin-right: 10px;">
|
||||||
Kopiera föregående månad
|
Kopiera föregående månad
|
||||||
</button>
|
</button>
|
||||||
|
<button ng-click="copyBudget()" style="margin-right: 10px;">
|
||||||
|
Kopiera befintlig budget
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal-backdrop" ng-show="showCopyModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Kopiera budget</h3>
|
||||||
|
<select ng-model="selectedBudgetToCopy"
|
||||||
|
ng-options="b as formatBudgetName(b) for b in budgetList track by b.id">
|
||||||
|
<option value="">Välj budget</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<button ng-click="confirmCopyBudget()">Kopiera</button>
|
||||||
|
<button ng-click="showCopyModal = false">Avbryt</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="add-item-popup" ng-show="addPopupVisible" ng-style="addPopupStyle" ng-class="{ 'above': addPopupAbove }">
|
<div class="add-item-popup" ng-show="addPopupVisible" ng-style="addPopupStyle" ng-class="{ 'above': addPopupAbove }">
|
||||||
<label>Typ:</label>
|
<label>Typ:</label>
|
||||||
<select ng-model="addPopupData.newItemType">
|
<select ng-model="addPopupData.newItemType">
|
||||||
@@ -272,7 +329,8 @@
|
|||||||
<script>
|
<script>
|
||||||
window.initialYear = @(ViewBag.Year ?? "null");
|
window.initialYear = @(ViewBag.Year ?? "null");
|
||||||
window.initialMonth = @(ViewBag.Month ?? "null");
|
window.initialMonth = @(ViewBag.Month ?? "null");
|
||||||
window.initialName = "@(ViewBag.BudgetName ?? "")";
|
window.initialName = "@Html.Raw(ViewBag.BudgetName ?? "")";
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="~/js/budget.js"></script>
|
<script src="~/js/budget.js"></script>
|
||||||
|
|||||||
120
Aberwyn/Views/Budget/List.cshtml
Normal file
120
Aberwyn/Views/Budget/List.cshtml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@attribute [Authorize(Roles = "Budget")]
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Budgetlista";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div ng-app="budgetApp" ng-controller="BudgetListController" class="budget-page">
|
||||||
|
|
||||||
|
<!-- Toggle -->
|
||||||
|
<div class="details-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="showDetails"> Visa mer detaljer
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-repeat="(year, months) in monthsByYear">
|
||||||
|
<div class="year-header">{{ year }}</div>
|
||||||
|
|
||||||
|
<div class="budget-row">
|
||||||
|
<div class="budget-card" ng-repeat="month in months" ng-click="goToBudget(month)">
|
||||||
|
<!-- Flex-container med namn + bars -->
|
||||||
|
<div class="month-header">
|
||||||
|
<div class="month-name">{{ getMonthName(month) }}</div>
|
||||||
|
<div class="month-bars">
|
||||||
|
<div class="bar income" ng-style="{'height': month.barHeights.income + '%'}" title="Inkomst"></div>
|
||||||
|
<div class="bar expenses" ng-style="{'height': month.barHeights.expenses + '%'}" title="Utgifter"></div>
|
||||||
|
<div class="bar savings" ng-style="{'height': month.barHeights.savings + '%'}" title="Sparande"></div>
|
||||||
|
<!-- <div class="bar leftover" ng-style="{'height': month.barHeights.leftover + '%'}" title="Balans"></div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-list" ng-if="showDetails">
|
||||||
|
<div class="item-row" ng-repeat="cat in month.categories">
|
||||||
|
<span class="item-label" style="color: {{ cat.color }}">{{ cat.name }}</span>
|
||||||
|
<span class="amount">{{ getCategorySum(cat) | number:0 }} kr</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="~/css/budget-list.css" />
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
||||||
|
<script>
|
||||||
|
angular.module('budgetApp', [])
|
||||||
|
.controller('BudgetListController', ['$scope', '$http', '$window', function($scope, $http, $window){
|
||||||
|
|
||||||
|
$scope.months = [];
|
||||||
|
$scope.monthsByYear = {};
|
||||||
|
$scope.showDetails = false;
|
||||||
|
const maxBarValue = 100000;
|
||||||
|
const monthNames = ["Januari","Februari","Mars","April","Maj","Juni",
|
||||||
|
"Juli","Augusti","September","Oktober","November","December"];
|
||||||
|
|
||||||
|
$scope.getMonthName = month => month.month ? monthNames[month.month-1] : month.name;
|
||||||
|
$scope.getCategorySum = cat => cat.items ? cat.items.reduce((sum,i)=>sum+i.amount,0) : 0;
|
||||||
|
$scope.goToBudget = month => $window.location.href = '/budget/' + (month.year || month.name) + '/' + (month.month || '');
|
||||||
|
|
||||||
|
$scope.getTotalIncome = period => {
|
||||||
|
return (period.categories || [])
|
||||||
|
.flatMap(c => c.items || [])
|
||||||
|
.filter(i => !i.isExpense && i.includeInSummary)
|
||||||
|
.reduce((sum,i)=>sum+i.amount,0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getTotalSavings = period => {
|
||||||
|
return (period.categories || [])
|
||||||
|
.flatMap(c => c.items || [])
|
||||||
|
.filter(i => !i.isExpense && i.includeInSummary && i.name.toLowerCase().includes('spara'))
|
||||||
|
.reduce((sum,i)=>sum+i.amount,0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getTotalExpenses = period => {
|
||||||
|
return (period.categories || [])
|
||||||
|
.flatMap(c => c.items || [])
|
||||||
|
.filter(i => i.isExpense && i.includeInSummary)
|
||||||
|
.reduce((sum,i)=>sum+i.amount,0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getTotalLeftover = period => {
|
||||||
|
return $scope.getTotalIncome(period) - $scope.getTotalSavings(period) - $scope.getTotalExpenses(period);
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.get('/api/budget/list').then(res => {
|
||||||
|
// Sortera från januari → december per år
|
||||||
|
const sorted = res.data.sort((a,b) => (a.year||0)-(b.year||0) || (a.month||0)-(b.month||0));
|
||||||
|
|
||||||
|
$scope.months = sorted.map(month => {
|
||||||
|
const income = $scope.getTotalIncome(month);
|
||||||
|
const savings = $scope.getTotalSavings(month);
|
||||||
|
const expenses = $scope.getTotalExpenses(month);
|
||||||
|
const leftover = $scope.getTotalLeftover(month);
|
||||||
|
|
||||||
|
month.barHeights = {
|
||||||
|
income: Math.max(income / maxBarValue * 100, 5),
|
||||||
|
savings: Math.max(savings / maxBarValue * 100, 5),
|
||||||
|
expenses: Math.max(expenses / maxBarValue * 100, 5),
|
||||||
|
leftover: Math.max(leftover / maxBarValue * 100, 5)
|
||||||
|
};
|
||||||
|
return month;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gruppera per år
|
||||||
|
$scope.monthsByYear = $scope.months.reduce((acc, m) => {
|
||||||
|
const year = m.year || 'Övrigt';
|
||||||
|
if (!acc[year]) acc[year] = [];
|
||||||
|
acc[year].push(m);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}]);
|
||||||
|
</script>
|
||||||
144
Aberwyn/Views/FoodMenu/Calculator.cshtml
Normal file
144
Aberwyn/Views/FoodMenu/Calculator.cshtml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
@model Aberwyn.Models.DoughPlan
|
||||||
|
@{
|
||||||
|
var plans = ViewBag.Plans as List<Aberwyn.Models.DoughPlan>;
|
||||||
|
}
|
||||||
|
<div class="card border-info mb-4">
|
||||||
|
<div class="card-header">🍕 Pizzakalkylator</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="calcForm" class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
|
<div>
|
||||||
|
<label>Antal pizzor</label>
|
||||||
|
<input id="antal" type="number" class="form-control" value="8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Vikt per pizza (g)</label>
|
||||||
|
<input id="vikt" type="number" class="form-control" value="220" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Datum</label>
|
||||||
|
<input id="datum" type="date" class="form-control" value="@DateTime.Today.ToString("yyyy-MM-dd")" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Namn på tillfälle</label>
|
||||||
|
<input id="namn" class="form-control" value="Planering" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button type="button" class="btn btn-success mt-3" id="saveBtn">💾 Spara</button>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h5>Resultat</h5>
|
||||||
|
<div class="completed-orders-grid">
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Totalt deg</strong>
|
||||||
|
<span id="totalDeg"></span> g
|
||||||
|
</div>
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Mjöl</strong>
|
||||||
|
<span id="mjol"></span> g
|
||||||
|
</div>
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Vatten</strong>
|
||||||
|
<span id="vatten"></span> g
|
||||||
|
</div>
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Olja</strong>
|
||||||
|
<span id="olja"></span> g
|
||||||
|
</div>
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Salt</strong>
|
||||||
|
<span id="salt"></span> g
|
||||||
|
</div>
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>Jäst</strong>
|
||||||
|
<span id="jast"></span> g
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (plans?.Any() == true)
|
||||||
|
{
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-header">📋 Sparade planer</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="completed-orders-grid">
|
||||||
|
@foreach (var p in plans)
|
||||||
|
{
|
||||||
|
<div class="completed-order-box">
|
||||||
|
<strong>@p.Namn (@p.Datum.ToString("yyyy-MM-dd"))</strong>
|
||||||
|
<span>🍕 @p.AntalPizzor st × @p.ViktPerPizza:F1 g</span>
|
||||||
|
<hr style="margin:6px 0;" />
|
||||||
|
<span><b>Totalt:</b> @p.TotalDeg:F1 g</span>
|
||||||
|
<span>Mjöl: @p.Mjol:F1 g</span>
|
||||||
|
<span>Vatten: @p.Vatten:F1 g</span>
|
||||||
|
<span>Olja: @p.Olja:F1 g</span>
|
||||||
|
<span>Salt: @p.Salt:F1 g</span>
|
||||||
|
<span>Jäst: @p.Jast:F1 g</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<link rel="stylesheet" href="~/css/pizzacalculator.css" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function round1(x) {
|
||||||
|
return Math.round(x * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calc() {
|
||||||
|
let antal = parseFloat(document.getElementById("antal").value) || 0;
|
||||||
|
let vikt = parseFloat(document.getElementById("vikt").value) || 0;
|
||||||
|
let total = antal * vikt;
|
||||||
|
let mjol = total * (100.0 / 162.0);
|
||||||
|
let vatten = mjol * 0.52;
|
||||||
|
let olja = mjol * 0.075;
|
||||||
|
let salt = mjol * 0.02;
|
||||||
|
let jast = mjol * 0.005;
|
||||||
|
|
||||||
|
document.getElementById("totalDeg").innerText = round1(total);
|
||||||
|
document.getElementById("mjol").innerText = round1(mjol);
|
||||||
|
document.getElementById("vatten").innerText = round1(vatten);
|
||||||
|
document.getElementById("olja").innerText = round1(olja);
|
||||||
|
document.getElementById("salt").innerText = round1(salt);
|
||||||
|
document.getElementById("jast").innerText = round1(jast);
|
||||||
|
|
||||||
|
return { antal, vikt, total, mjol, vatten, olja, salt, jast };
|
||||||
|
}
|
||||||
|
|
||||||
|
["antal","vikt"].forEach(id => {
|
||||||
|
document.getElementById(id).addEventListener("input", calc);
|
||||||
|
});
|
||||||
|
|
||||||
|
calc();
|
||||||
|
|
||||||
|
document.getElementById("saveBtn").addEventListener("click", function () {
|
||||||
|
let values = calc();
|
||||||
|
let data = {
|
||||||
|
AntalPizzor: values.antal,
|
||||||
|
ViktPerPizza: values.vikt,
|
||||||
|
TotalDeg: values.total,
|
||||||
|
Mjol: values.mjol,
|
||||||
|
Vatten: values.vatten,
|
||||||
|
Olja: values.olja,
|
||||||
|
Salt: values.salt,
|
||||||
|
Jast: values.jast,
|
||||||
|
Datum: document.getElementById("datum").value,
|
||||||
|
Namn: document.getElementById("namn").value
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('@Url.Action("SaveDoughPlan", "FoodMenu")', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then(r => r.json()).then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
location.reload(); // 🔄 Ladda om sidan så listan uppdateras
|
||||||
|
} else {
|
||||||
|
alert("❌ Kunde inte spara");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,31 +1,27 @@
|
|||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@model Aberwyn.Models.WeeklyMenuViewModel
|
@model Aberwyn.Models.WeeklyMenuViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Veckomeny";
|
ViewData["Title"] = "Veckomeny";
|
||||||
var days = new[] { "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag" };
|
var days = new[] { "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag" };
|
||||||
}
|
}
|
||||||
|
<html lang="sv" ng-app="app">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Veckomeny</title>
|
||||||
<link rel="stylesheet" href="~/css/Veckomeny.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/Veckomeny.css" asp-append-version="true" />
|
||||||
</head>
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.knownMeals = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
|
window.knownMeals = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
|
||||||
(ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>)?.Select(m => m.Name.Trim()).ToList() ?? new List<string>()));
|
(ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>)?.Select(m => m.Name.Trim()).ToList() ?? new List<string>()));
|
||||||
</script>
|
</script>
|
||||||
|
</head>
|
||||||
|
<body x-data="recentHistory()" x-init="init()">
|
||||||
|
|
||||||
<div class="weekly-menu-wrapper"
|
<div class="menu-wishlist-wrapper">
|
||||||
x-data="{
|
|
||||||
highlightNew(event) {
|
|
||||||
const input = event.target;
|
|
||||||
const val = input.value?.trim().toLowerCase();
|
|
||||||
input.classList.remove('new-entry', 'existing-entry');
|
|
||||||
|
|
||||||
if (!val) return;
|
<!-- Veckomenyn -->
|
||||||
|
|
||||||
const isKnown = window.knownMeals.some(name => name.toLowerCase() === val);
|
|
||||||
input.classList.add(isKnown ? 'existing-entry' : 'new-entry');
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<section class="weekly-editor">
|
<section class="weekly-editor">
|
||||||
<h1>Veckomeny - Vecka @Model.WeekNumber</h1>
|
<h1>Veckomeny - Vecka @Model.WeekNumber</h1>
|
||||||
<div class="week-nav">
|
<div class="week-nav">
|
||||||
@@ -46,7 +42,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (int i = 0; i < 7; i++) {
|
@for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
var dinnerEntry = Model.GetMealEntry(i, "Middag");
|
var dinnerEntry = Model.GetMealEntry(i, "Middag");
|
||||||
var lunchEntry = Model.GetMealEntry(i, "Lunch");
|
var lunchEntry = Model.GetMealEntry(i, "Lunch");
|
||||||
var breakfastEntry = Model.GetMealEntry(i, "Frukost");
|
var breakfastEntry = Model.GetMealEntry(i, "Frukost");
|
||||||
@@ -61,8 +58,16 @@
|
|||||||
<td class="meal-cell">
|
<td class="meal-cell">
|
||||||
<div class="meal-entry-group">
|
<div class="meal-entry-group">
|
||||||
<div class="meal-input-group">
|
<div class="meal-input-group">
|
||||||
<input type="text" name="Meal[@i][Middag]" value="@dinnerEntry?.DinnerMealName" placeholder="Lägg till middag..." list="meals-list" class="meal-input" :tabindex="showExtra ? 0 : -1" x-on:input="highlightNew($event)" />
|
<input type="text"
|
||||||
<button type="button" class="delete-btn" title="Rensa middag" onclick="this.previousElementSibling.value='';">
|
name="Meal[@i][Middag]"
|
||||||
|
value="@dinnerEntry?.DinnerMealName"
|
||||||
|
placeholder="Lägg till middag..."
|
||||||
|
list="meals-list"
|
||||||
|
class="meal-input"
|
||||||
|
:tabindex="showExtra ? 0 : -1"
|
||||||
|
x-on:input="highlightFromCurrentWeek()" />
|
||||||
|
<button type="button" class="delete-btn" title="Rensa middag"
|
||||||
|
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +76,8 @@
|
|||||||
<td class="cook-cell">
|
<td class="cook-cell">
|
||||||
<select name="Cook[@i]" :tabindex="showExtra ? 0 : -1" class="meal-input">
|
<select name="Cook[@i]" :tabindex="showExtra ? 0 : -1" class="meal-input">
|
||||||
<option value="">Välj kock</option>
|
<option value="">Välj kock</option>
|
||||||
@foreach(var user in Model.AvailableCooks) {
|
@foreach (var user in Model.AvailableCooks)
|
||||||
|
{
|
||||||
var selected = Model.WeeklyMenus.FirstOrDefault(m => m.DayOfWeek == i + 1)?.Cook == user.Username;
|
var selected = Model.WeeklyMenus.FirstOrDefault(m => m.DayOfWeek == i + 1)?.Cook == user.Username;
|
||||||
<option value="@user.Username" selected="@selected">@user.Name</option>
|
<option value="@user.Username" selected="@selected">@user.Name</option>
|
||||||
}
|
}
|
||||||
@@ -84,15 +90,29 @@
|
|||||||
<div class="extra-meals">
|
<div class="extra-meals">
|
||||||
<div class="meal-input-group">
|
<div class="meal-input-group">
|
||||||
<label>Frukost:</label>
|
<label>Frukost:</label>
|
||||||
<input type="text" name="Meal[@i][Frukost]" value="@breakfastEntry?.BreakfastMealName" placeholder="Lägg till frukost..." list="meals-list" class="meal-input" x-on:input="highlightNew($event)" />
|
<input type="text"
|
||||||
<button type="button" class="delete-btn" title="Rensa frukost" onclick="this.previousElementSibling.value='';">
|
name="Meal[@i][Frukost]"
|
||||||
|
value="@breakfastEntry?.BreakfastMealName"
|
||||||
|
placeholder="Lägg till frukost..."
|
||||||
|
list="meals-list"
|
||||||
|
class="meal-input"
|
||||||
|
x-on:input="highlightFromCurrentWeek()" />
|
||||||
|
<button type="button" class="delete-btn" title="Rensa frukost"
|
||||||
|
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="meal-input-group">
|
<div class="meal-input-group">
|
||||||
<label>Lunch:</label>
|
<label>Lunch:</label>
|
||||||
<input type="text" name="Meal[@i][Lunch]" value="@lunchEntry?.LunchMealName" placeholder="Lägg till lunch..." list="meals-list" class="meal-input" x-on:input="highlightNew($event)" />
|
<input type="text"
|
||||||
<button type="button" class="delete-btn" title="Rensa lunch" onclick="this.previousElementSibling.value='';">
|
name="Meal[@i][Lunch]"
|
||||||
|
value="@lunchEntry?.LunchMealName"
|
||||||
|
placeholder="Lägg till lunch..."
|
||||||
|
list="meals-list"
|
||||||
|
class="meal-input"
|
||||||
|
x-on:input="highlightFromCurrentWeek()" />
|
||||||
|
<button type="button" class="delete-btn" title="Rensa lunch"
|
||||||
|
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,51 +123,167 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="save-menu-btn">
|
||||||
|
Spara
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<datalist id="meals-list">
|
||||||
|
@foreach (var meal in (ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>) ?? new List<Aberwyn.Models.Meal>())
|
||||||
|
{
|
||||||
|
<option value="@meal.Name.Trim()"></option>
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Wishlist -->
|
||||||
|
<aside class="wishlist" ng-controller="WishListCtrl as vm" ng-init="vm.loadWishes()">
|
||||||
|
<h2>Önskemåltider</h2>
|
||||||
|
<div class="wishlist-grid">
|
||||||
|
<div class="wishlist-card" ng-repeat="wish in vm.WishList track by wish.Id">
|
||||||
|
<div class="wishlist-header">
|
||||||
|
<strong>{{wish.Name}}</strong>
|
||||||
|
<span class="wishlist-meta">
|
||||||
|
av {{wish.RequestedByUserName}} • {{wish.CreatedAt}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="wishlist-body" ng-if="wish.Recipe">
|
||||||
|
<p>{{wish.Recipe | limitTo:100}}{{wish.Recipe.length > 100 ? '...' : ''}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="wishlist-actions">
|
||||||
|
<button type="button" ng-click="vm.importWish(wish.Id)" class="btn import">
|
||||||
|
<i class="fa-solid fa-plus"></i> Importera
|
||||||
|
</button>
|
||||||
|
<button type="button" ng-click="vm.archiveWish(wish.Id)" class="btn archive">
|
||||||
|
<i class="fa-solid fa-box-archive"></i> Arkivera
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p ng-if="!vm.WishList.length">Inga önskemål just nu.</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div class="add-meal-wrapper">
|
|
||||||
<button type="submit" class="save-menu-btn">Spara veckomeny</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="recent-history">
|
<!-- Översikt senaste 4 veckor, full width -->
|
||||||
<h2>Översikt senaste 4 veckor</h2>
|
<aside class="recent-history-fullwidth">
|
||||||
@{
|
<h2>Översikt senaste 4 veckor (middag)</h2>
|
||||||
var lastWeeks = Enumerable.Range(1, 4)
|
<div class="recent-table-wrapper">
|
||||||
.Select(i => DateTime.Now.AddDays(-7 * i))
|
|
||||||
.Select(dt => new { Year = dt.Year, Week = ISOWeek.GetWeekOfYear(dt) })
|
|
||||||
.Distinct().ToList();
|
|
||||||
}
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Vecka</th>
|
<th>Vecka</th>
|
||||||
@foreach (var d in days) {
|
@foreach (var d in days)
|
||||||
|
{
|
||||||
<th>@d</th>
|
<th>@d</th>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var w in lastWeeks) {
|
<template x-for="week in weeks" :key="week.weekNumber">
|
||||||
<tr>
|
<tr>
|
||||||
<td>@w.Week</td>
|
<td x-text="week.weekNumber"></td>
|
||||||
@for (int idx = 1; idx <= 7; idx++) {
|
<template x-for="day in week.days">
|
||||||
var entry = Model.RecentEntries?.FirstOrDefault(e =>
|
<td x-text="day.dinner" :class="day.highlight ? 'highlight' : ''"></td>
|
||||||
e.Date.Year == w.Year &&
|
</template>
|
||||||
ISOWeek.GetWeekOfYear(e.Date) == w.Week &&
|
|
||||||
((int)e.Date.DayOfWeek == (idx % 7)));
|
|
||||||
<td>@(entry?.DinnerMealName ?? "—")</td>
|
|
||||||
}
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</aside>
|
|
||||||
|
|
||||||
<datalist id="meals-list">
|
|
||||||
@foreach (var meal in (List<Meal>)ViewBag.AvailableMeals)
|
|
||||||
{
|
|
||||||
<option value="@meal.Name">@meal.Name</option>
|
|
||||||
}
|
|
||||||
</datalist>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
<script>
|
||||||
|
function recentHistory() {
|
||||||
|
return {
|
||||||
|
weeks: [],
|
||||||
|
init() {
|
||||||
|
fetch('/FoodMenu/GetRecentMenuEntries?weeksBack=4')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const grouped = {};
|
||||||
|
data.forEach(e => {
|
||||||
|
const dt = new Date(e.Date);
|
||||||
|
const weekNo = getISOWeek(dt);
|
||||||
|
const dayIdx = dt.getDay() === 0 ? 6 : dt.getDay() - 1;
|
||||||
|
const key = `W${weekNo}`;
|
||||||
|
if (!grouped[key]) grouped[key] = Array(7).fill().map(() => ({ dinner: '—', highlight: false }));
|
||||||
|
grouped[key][dayIdx] = { dinner: e.DinnerMealName?.trim() || '—', highlight: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.weeks = Object.keys(grouped)
|
||||||
|
.sort((a,b) => b.localeCompare(a))
|
||||||
|
.map(weekKey => ({ weekNumber: parseInt(weekKey.replace('W','')), days: grouped[weekKey] }));
|
||||||
|
|
||||||
|
this.highlightFromCurrentWeek();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getISOWeek(date) {
|
||||||
|
const tmpDate = new Date(date.getTime());
|
||||||
|
tmpDate.setHours(0,0,0,0);
|
||||||
|
tmpDate.setDate(tmpDate.getDate() + 4 - (tmpDate.getDay()||7));
|
||||||
|
const yearStart = new Date(tmpDate.getFullYear(),0,1);
|
||||||
|
return Math.ceil((((tmpDate - yearStart) / 86400000) + 1)/7);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightMeal(inputValue) {
|
||||||
|
const val = inputValue?.trim().toLowerCase();
|
||||||
|
this.weeks.forEach(week => {
|
||||||
|
week.days.forEach(day => {
|
||||||
|
day.highlight = val && day.dinner?.toLowerCase() === val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightFromCurrentWeek() {
|
||||||
|
// hämta alla inputs som har något värde
|
||||||
|
const values = Array.from(document.querySelectorAll(".meal-input"))
|
||||||
|
.map(i => i.value?.trim().toLowerCase())
|
||||||
|
.filter(v => v);
|
||||||
|
|
||||||
|
this.weeks.forEach(week => {
|
||||||
|
week.days.forEach(day => {
|
||||||
|
day.highlight = values.includes(day.dinner?.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
angular.module('app', [])
|
||||||
|
.controller('WishListCtrl', ['$http', function($http){
|
||||||
|
var vm = this;
|
||||||
|
vm.WishList = [];
|
||||||
|
|
||||||
|
vm.loadWishes = function(){
|
||||||
|
$http.get('/api/MealWish/all')
|
||||||
|
.then(function(res){
|
||||||
|
vm.WishList = res.data;
|
||||||
|
}, function(err){
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.archiveWish = function(id){
|
||||||
|
$http.post('/api/MealWish/' + id + '/archive')
|
||||||
|
.then(function(){
|
||||||
|
vm.WishList = vm.WishList.filter(function(w){ return w.Id !== id; });
|
||||||
|
}, function(err){
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.importWish = function(id){
|
||||||
|
$http.post('/api/MealWish/' + id + '/import')
|
||||||
|
.then(function(){
|
||||||
|
vm.WishList = vm.WishList.filter(function(w){ return w.Id !== id; });
|
||||||
|
}, function(err){
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="school-meals card-view">
|
<div class="school-meals card-view">
|
||||||
<h2>Skolmat</h2>
|
<h2>Skolmat</h2>
|
||||||
|
|
||||||
@@ -86,7 +86,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
|
<!-- Floating button för att öppna popup -->
|
||||||
|
<button class="floating-btn" ng-click="toggleWishFlyout()" ng-if="isLoggedIn">+</button>
|
||||||
|
<!-- Popup för önskad rätt -->
|
||||||
|
<div class="wish-flyout" ng-show="showWishFlyout">
|
||||||
|
<form ng-submit="submitWish()">
|
||||||
|
<h3>Önska mat</h3>
|
||||||
|
<label for="wishName">Namn</label>
|
||||||
|
<input type="text"
|
||||||
|
ng-model="newWishName"
|
||||||
|
placeholder="Skriv eller sök rätt"
|
||||||
|
ng-change="checkExistingMeal()"
|
||||||
|
autocomplete="off">
|
||||||
|
|
||||||
|
<!-- Autocomplete-lista -->
|
||||||
|
<ul class="autocomplete-list" ng-if="existingMeals.length > 0">
|
||||||
|
<li ng-repeat="meal in existingMeals" ng-click="selectExistingMeal(meal)">
|
||||||
|
{{ meal.Name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Receptfält visas bara om ingen befintlig rätt hittas -->
|
||||||
|
<textarea ng-if="showRecipeField" ng-model="newWishRecipe" placeholder="Skriv recept"></textarea>
|
||||||
|
|
||||||
|
<button type="submit">Skicka önskan</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
214
Aberwyn/Views/Report/Budget.cshtml
Normal file
214
Aberwyn/Views/Report/Budget.cshtml
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Budget Översikt</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="~/css/budget-overview.css" />
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body ng-app="budgetApp" ng-controller="BudgetOverviewController" class="budget-report-overview-body" ng-cloak>
|
||||||
|
|
||||||
|
<h2>Visa kolumner</h2>
|
||||||
|
|
||||||
|
<div class="budget-column-selector-wrapper">
|
||||||
|
<!-- Kategori-definitioner -->
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<span>Kategori-definitioner:</span>
|
||||||
|
<input type="text" ng-model="searchCategoryDef" placeholder="Sök..."
|
||||||
|
ng-focus="openDropdown('catDef')" ng-click="$event.stopPropagation()" />
|
||||||
|
<div class="budget-dropdown-menu" ng-show="dropdownOpenCatDef">
|
||||||
|
<label ng-repeat="col in categoryDefColumns | filter:searchCategoryDef track by $index">
|
||||||
|
<input type="checkbox" ng-model="selectedCategoryDef[col]" /> {{ col }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategori-namn -->
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<span>Kategori-namn:</span>
|
||||||
|
<input type="text" ng-model="searchCategoryLabel" placeholder="Sök..."
|
||||||
|
ng-focus="openDropdown('catLabel')" ng-click="$event.stopPropagation()" />
|
||||||
|
<div class="budget-dropdown-menu" ng-show="dropdownOpenCatLabel">
|
||||||
|
<label ng-repeat="col in categoryLabelColumns | filter:searchCategoryLabel track by $index">
|
||||||
|
<input type="checkbox" ng-model="selectedCategoryLabel[col]" /> {{ col }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item-definitioner -->
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<span>Item-definitioner:</span>
|
||||||
|
<input type="text" ng-model="searchItemDef" placeholder="Sök..."
|
||||||
|
ng-focus="openDropdown('itemDef')" ng-click="$event.stopPropagation()" />
|
||||||
|
<div class="budget-dropdown-menu" ng-show="dropdownOpenItemDef">
|
||||||
|
<label ng-repeat="col in itemDefColumns | filter:searchItemDef track by $index">
|
||||||
|
<input type="checkbox" ng-model="selectedItemDef[col]" /> {{ col }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item-namn -->
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<span>Item-namn:</span>
|
||||||
|
<input type="text" ng-model="searchItemLabel" placeholder="Sök..."
|
||||||
|
ng-focus="openDropdown('itemLabel')" ng-click="$event.stopPropagation()" />
|
||||||
|
<div class="budget-dropdown-menu" ng-show="dropdownOpenItemLabel">
|
||||||
|
<label ng-repeat="col in itemLabelColumns | filter:searchItemLabel track by $index">
|
||||||
|
<input type="checkbox" ng-model="selectedItemLabel[col]" /> {{ col }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Månadssökfält med checkboxar och popup -->
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<span>Visa månader:</span>
|
||||||
|
<input type="text" ng-model="searchMonth" placeholder="Sök månad eller år..."
|
||||||
|
ng-focus="dropdownOpenMonth = true"
|
||||||
|
ng-click="$event.stopPropagation()" />
|
||||||
|
<div class="budget-dropdown-menu" ng-show="dropdownOpenMonth">
|
||||||
|
<label ng-repeat="month in allMonths | filter:searchMonth track by month.id">
|
||||||
|
<input type="checkbox" ng-model="selectedMonths[month.id]" /> {{ month.year }} - {{ month.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="splitByYear" /> Dela upp per år
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="budget-column-selector">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="showTotals" /> Visa Inkomst / Utgift / Netto
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div ng-if="!years.length" class="budget-loading-indicator">Laddar data…</div>
|
||||||
|
|
||||||
|
<!-- Budgetblad -->
|
||||||
|
<div ng-repeat="yearData in (splitByYear ? years : flatYears) | orderBy:'-year' track by yearData.year">
|
||||||
|
<div class="budget-year-header">{{ yearData.year }}</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="budget-sheet-header">
|
||||||
|
<div class="budget-sheet-cell month">Månad</div>
|
||||||
|
<div class="budget-sheet-cell income" ng-if="showTotals">Inkomst</div>
|
||||||
|
<div class="budget-sheet-cell expense" ng-if="showTotals">Utgift</div>
|
||||||
|
<div class="budget-sheet-cell net" ng-if="showTotals">Netto</div>
|
||||||
|
|
||||||
|
<div class="budget-extra-columns">
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryDef track by $index"
|
||||||
|
ng-if="selectedCategoryDef[colName]"
|
||||||
|
ng-style="{'background-color': categoryColors.catDef[colName]}">
|
||||||
|
{{ colName }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryLabel track by $index"
|
||||||
|
ng-if="selectedCategoryLabel[colName]"
|
||||||
|
ng-style="{'background-color': categoryColors.catLabel[colName]}">
|
||||||
|
{{ colName }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemDef track by $index"
|
||||||
|
ng-if="selectedItemDef[colName]">
|
||||||
|
{{ colName }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemLabel track by $index"
|
||||||
|
ng-if="selectedItemLabel[colName]">
|
||||||
|
{{ colName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rader -->
|
||||||
|
<div class="budget-sheet-row" ng-repeat="month in yearData.months | filter:filterMonths track by month.id">
|
||||||
|
|
||||||
|
<div class="budget-sheet-cell month">{{ month.year }} - {{ monthNames[month.month-1] }}</div>
|
||||||
|
<div class="budget-sheet-cell income" ng-if="showTotals"
|
||||||
|
ng-class="{'budget-positive': month.income >=0, 'budget-negative': month.income <0}">
|
||||||
|
{{ month.income | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell expense" ng-if="showTotals"
|
||||||
|
ng-class="{'budget-positive': month.expense >=0, 'budget-negative': month.expense <0}">
|
||||||
|
{{ month.expense | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell net" ng-if="showTotals"
|
||||||
|
ng-class="{'budget-positive': month.net >=0, 'budget-negative': month.net <0}">
|
||||||
|
{{ month.net | number:0 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="budget-extra-columns">
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryDef track by $index"
|
||||||
|
ng-if="selectedCategoryDef[colName]"
|
||||||
|
ng-class="{'budget-positive': month.categoryDefinitions[colName] >=0, 'budget-negative': month.categoryDefinitions[colName] <0}">
|
||||||
|
{{ month.categoryDefinitions[colName] || 0 | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryLabel track by $index"
|
||||||
|
ng-if="selectedCategoryLabel[colName]"
|
||||||
|
ng-class="{'budget-positive': month.categoryLabels[colName] >=0, 'budget-negative': month.categoryLabels[colName] <0}">
|
||||||
|
{{ month.categoryLabels[colName] || 0 | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemDef track by $index"
|
||||||
|
ng-if="selectedItemDef[colName]"
|
||||||
|
ng-class="{'budget-positive': month.itemDefinitions[colName] >=0, 'budget-negative': month.itemDefinitions[colName] <0}">
|
||||||
|
{{ month.itemDefinitions[colName] || 0 | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemLabel track by $index"
|
||||||
|
ng-if="selectedItemLabel[colName]"
|
||||||
|
ng-class="{'budget-positive': month.itemLabels[colName] >=0, 'budget-negative': month.itemLabels[colName] <0}">
|
||||||
|
{{ month.itemLabels[colName] || 0 | number:0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Totalsumma -->
|
||||||
|
<div class="budget-sheet-row budget-total-row" ng-if="yearData.months.length">
|
||||||
|
<div class="budget-sheet-cell month">Total</div>
|
||||||
|
<div class="budget-sheet-cell income" ng-if="showTotals">
|
||||||
|
{{ getYearTotal(yearData.months, 'income') | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell expense" ng-if="showTotals">
|
||||||
|
{{ getYearTotal(yearData.months, 'expense') | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell net" ng-if="showTotals">
|
||||||
|
{{ getYearTotal(yearData.months, 'net') | number:0 }}
|
||||||
|
</div>ö
|
||||||
|
|
||||||
|
<div class="budget-extra-columns">
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryDef track by $index"
|
||||||
|
ng-if="selectedCategoryDef[colName]">
|
||||||
|
{{ getYearTotal(yearData.months, 'categoryDefinitions', colName) | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.categoryLabel track by $index"
|
||||||
|
ng-if="selectedCategoryLabel[colName]">
|
||||||
|
{{ getYearTotal(yearData.months, 'categoryLabels', colName) | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemDef track by $index"
|
||||||
|
ng-if="selectedItemDef[colName]">
|
||||||
|
{{ getYearTotal(yearData.months, 'itemDefinitions', colName) | number:0 }}
|
||||||
|
</div>
|
||||||
|
<div class="budget-sheet-cell other"
|
||||||
|
ng-repeat="colName in columnOrder.itemLabel track by $index"
|
||||||
|
ng-if="selectedItemLabel[colName]">
|
||||||
|
{{ getYearTotal(yearData.months, 'itemLabels', colName) | number:0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
||||||
|
<script src="~/js/budget-overview.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
@{
|
|
||||||
ViewData["Title"] = "Budgetrapport";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div ng-app="reportApp" ng-controller="ReportController" class="report-page" ng-init="init()">
|
|
||||||
<h1>Budgetrapport</h1>
|
|
||||||
|
|
||||||
<div class="report-controls">
|
|
||||||
<div class="date-select">
|
|
||||||
<label>Från:</label>
|
|
||||||
<select ng-model="startYear" ng-options="y for y in years"></select>
|
|
||||||
<select ng-model="startMonth" ng-options="m.value as m.label for m in months"></select>
|
|
||||||
<label>till:</label>
|
|
||||||
<select ng-model="endYear" ng-options="y for y in years"></select>
|
|
||||||
<select ng-model="endMonth" ng-options="m.value as m.label for m in months"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="definition-select">
|
|
||||||
<label>Välj poster att inkludera:</label>
|
|
||||||
<div class="checkbox-grid">
|
|
||||||
<label ng-repeat="def in definitions">
|
|
||||||
<input type="checkbox" ng-model="def.Selected" /> {{ def.Name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn-generate" ng-click="loadReport()">Generera rapport</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="report-results" ng-if="results.length > 0">
|
|
||||||
<h2>Resultat</h2>
|
|
||||||
|
|
||||||
<canvas id="reportChart" width="100%" height="50"></canvas>
|
|
||||||
|
|
||||||
<table class="report-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>År</th>
|
|
||||||
<th>Månad</th>
|
|
||||||
<th ng-repeat="def in activeDefinitions">{{ def.Name }}</th>
|
|
||||||
<th>Totalt</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="row in results">
|
|
||||||
<td>{{ row.Year }}</td>
|
|
||||||
<td>{{ monthName(row.Month) }}</td>
|
|
||||||
<td ng-repeat="def in activeDefinitions">
|
|
||||||
{{ getAmount(row, def.Id) | number:0 }}
|
|
||||||
</td>
|
|
||||||
<td>{{ getRowTotal(row) | number:0 }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="~/css/report.css" />
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<script src="~/js/report.js"></script>
|
|
||||||
229
Aberwyn/Views/Rss/Index.cshtml
Normal file
229
Aberwyn/Views/Rss/Index.cshtml
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
@model TorrentListViewModel
|
||||||
|
<link rel="stylesheet" href="~/css/torrent.css" />
|
||||||
|
|
||||||
|
<!-- Periodval -->
|
||||||
|
<div class="torrent-period">
|
||||||
|
<button class="@(Model.CurrentRange == "day" ? "active" : "")" onclick="location.href='?range=day'">Dag</button>
|
||||||
|
<button class="@(Model.CurrentRange == "week" ? "active" : "")" onclick="location.href='?range=week'">Vecka</button>
|
||||||
|
<button class="@(Model.CurrentRange == "month" ? "active" : "")" onclick="location.href='?range=month'">Månad</button>
|
||||||
|
<button class="@(Model.CurrentRange == "all" ? "active" : "")" onclick="location.href='?range=all'">All time</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Torrentlista -->
|
||||||
|
<div class="torrent-list">
|
||||||
|
<div class="torrent-header">
|
||||||
|
<div onclick="sortBy('title')" class="@(Model.CurrentSort == "title" ? "active" : "")">Titel</div>
|
||||||
|
<div onclick="sortBy('date')" class="@(Model.CurrentSort == "date" ? "active" : "")">Tid / Datum</div>
|
||||||
|
<div onclick="sortBy('seeders')" class="@(Model.CurrentSort == "seeders" ? "active" : "")">Seeders</div>
|
||||||
|
<div onclick="sortBy('leechers')" class="@(Model.CurrentSort == "leechers" ? "active" : "")">Leechers</div>
|
||||||
|
<div>Åtgärd</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var group in Model.Items
|
||||||
|
.GroupBy(t => new { t.MovieName, t.Metadata?.Year })
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
MovieName = g.Key.MovieName,
|
||||||
|
Year = g.Key.Year,
|
||||||
|
Versions = g.OrderByDescending(t => t.Title.Contains("Fix") || t.Title.Contains("Repack"))
|
||||||
|
.ThenByDescending(t => t.Seeders)
|
||||||
|
.ToList()
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
var main = group.Versions.First();
|
||||||
|
var lastVersion = group.Versions.Last();
|
||||||
|
|
||||||
|
<!-- Huvudrad -->
|
||||||
|
<div class="torrent-row torrent-group-title @(group.Versions.Count == 1 ? "last-row" : "")">
|
||||||
|
<div class="col-title">
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.Poster) && main.Metadata.Poster != "N/A")
|
||||||
|
{
|
||||||
|
<a href="@main.Metadata.Poster" class="glightbox">
|
||||||
|
<img src="@main.Metadata.Poster"
|
||||||
|
alt="@main.MovieName"
|
||||||
|
class="poster"
|
||||||
|
onerror="this.onerror=null; this.src='/images/fallback.jpg';" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<img src="/images/fallback.jpg" alt="@main.MovieName" class="poster placeholder" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="title-info">
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.Title))
|
||||||
|
{
|
||||||
|
<strong>@group.MovieName (@group.Year) </strong>
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
<strong>@main.Title </strong>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (main.IsNew)
|
||||||
|
{
|
||||||
|
<img src="/images/new.png" alt="New" class="badge" />
|
||||||
|
}
|
||||||
|
<div class="meta">
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.Genre))
|
||||||
|
{
|
||||||
|
<span class="genre">@main.Metadata.Genre</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.ImdbID))
|
||||||
|
{
|
||||||
|
<a class="imdb" href="https://www.imdb.com/title/@main.Metadata.ImdbID" target="_blank">
|
||||||
|
⭐ @main.Metadata.ImdbRating
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (main.AvailableOn?.Any() == true)
|
||||||
|
{
|
||||||
|
<span class="available-on">
|
||||||
|
Tillgänglig på: @string.Join(", ", main.AvailableOn)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-date">
|
||||||
|
<div class="time">@main.PublishDate.ToString("HH:mm")</div>
|
||||||
|
<div class="date">@main.PublishDate.ToString("yyyy-MM-dd")</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-center @(main.Seeders > 40 ? "highlight-green" : "")">@main.Seeders</div>
|
||||||
|
<div class="col-center highlight-red">@main.Leechers</div>
|
||||||
|
<div class="col-action">
|
||||||
|
@if (main.IsDownloaded)
|
||||||
|
{
|
||||||
|
<span class="downloaded">✔ Nedladdad</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form class="download-form"
|
||||||
|
data-url="@Url.Action("Add", "Rss")"
|
||||||
|
data-torrent="@main.TorrentUrl">
|
||||||
|
<button type="submit" class="btn-add btn-small">➕ Ladda ner</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Versioner -->
|
||||||
|
@if (group.Versions.Count > 1)
|
||||||
|
{
|
||||||
|
foreach (var version in group.Versions.Skip(1))
|
||||||
|
{
|
||||||
|
var isLast = version == lastVersion;
|
||||||
|
<div class="torrent-row torrent-version @(isLast ? "last-version" : "")" title="@version.Title">
|
||||||
|
<div class="col-title">
|
||||||
|
<strong>
|
||||||
|
@version.Title
|
||||||
|
@if (version.IsNew)
|
||||||
|
{
|
||||||
|
<img src="/images/new.png" alt="New" class="badge" />
|
||||||
|
}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div>@version.PublishDate.ToString("HH:mm yyyy-MM-dd")</div>
|
||||||
|
<div class="@(version.Seeders > 40 ? "highlight-green" : "")">@version.Seeders</div>
|
||||||
|
<div class="highlight-red">@version.Leechers</div>
|
||||||
|
<div class="col-action">
|
||||||
|
<form asp-controller="RSS" asp-action="Add" method="post" onsubmit="return confirmDownload('@version.Title')">
|
||||||
|
<input type="hidden" name="torrentUrl" value="@version.TorrentUrl" />
|
||||||
|
<button type="submit" class="btn-add btn-small">➕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="pagination">
|
||||||
|
@for (int i = 1; i <= Model.TotalPages; i++)
|
||||||
|
{
|
||||||
|
if (i == Model.CurrentPage)
|
||||||
|
{
|
||||||
|
<span class="current">@i</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a asp-route-page="@i" asp-route-sort="@Model.CurrentSort" asp-route-range="@Model.CurrentRange">@i</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const lightbox = GLightbox({ selector: '.glightbox' });
|
||||||
|
function sortBy(field){
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('sort', field);
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll(".download-form").forEach(form => {
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault(); // 🔥 STOPPAR ALL REFRESH – ALLTID
|
||||||
|
|
||||||
|
console.log("form:", form);
|
||||||
|
const button = form.querySelector("button");
|
||||||
|
console.log("button:", button);
|
||||||
|
|
||||||
|
const torrentUrl = form.dataset.torrent;
|
||||||
|
const postUrl = form.dataset.url;
|
||||||
|
|
||||||
|
if (!torrentUrl || !postUrl) {
|
||||||
|
console.error("Saknar data-attribut");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm("Vill du ladda ner denna torrent?"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = "⏳ Laddar...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(postUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ torrentUrl })
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
console.log("Raw response:", text);
|
||||||
|
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
form.innerHTML = `<span class="downloaded">✔ Tillagd</span>`;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Misslyckades");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("JS error:", err);
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerText = "➕ Ladda ner";
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Ett fel uppstod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
118
Aberwyn/Views/Rss/_RssListPartial.cshtml
Normal file
118
Aberwyn/Views/Rss/_RssListPartial.cshtml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
@model RssListViewModel
|
||||||
|
|
||||||
|
<div class="rss-list">
|
||||||
|
<div class="rss-header">
|
||||||
|
<div onclick="sortBy('title')" class="@(Model.CurrentSort == "title" ? "active" : "")">Titel</div>
|
||||||
|
<div onclick="sortBy('date')" class="@(Model.CurrentSort == "date" ? "active" : "")">Tid / Datum</div>
|
||||||
|
<div onclick="sortBy('seeders')" class="@(Model.CurrentSort == "seeders" ? "active" : "")">Seeders</div>
|
||||||
|
<div onclick="sortBy('leechers')" class="@(Model.CurrentSort == "leechers" ? "active" : "")">Leechers</div>
|
||||||
|
<div>Åtgärd</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var group in Model.Items
|
||||||
|
.GroupBy(t => new { t.MovieName, t.Metadata?.Year })
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
MovieName = g.Key.MovieName,
|
||||||
|
Year = g.Key.Year,
|
||||||
|
Versions = g.OrderByDescending(t => t.Title.Contains("Fix") || t.Title.Contains("Repack"))
|
||||||
|
.ThenByDescending(t => t.Seeders)
|
||||||
|
.ToList()
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
var main = group.Versions.First();
|
||||||
|
var lastVersion = group.Versions.Last();
|
||||||
|
|
||||||
|
<!-- Huvudrad -->
|
||||||
|
<div class="rss-row rss-group-title @(group.Versions.Count == 1 ? "last-row" : "")">
|
||||||
|
<div class="col-title">
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.Poster) && main.Metadata.Poster != "N/A")
|
||||||
|
{
|
||||||
|
<a href="@main.Metadata.Poster" class="glightbox">
|
||||||
|
<img src="@main.Metadata.Poster"
|
||||||
|
alt="@main.MovieName"
|
||||||
|
class="poster"
|
||||||
|
onerror="this.onerror=null; this.src='/images/fallback.jpg';" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<img src="/images/fallback.jpg" alt="@main.MovieName" class="poster placeholder" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="title-info">
|
||||||
|
<strong>@(main.Metadata?.Title ?? group.MovieName) (@group.Year)</strong>
|
||||||
|
|
||||||
|
@if (main.IsNew)
|
||||||
|
{
|
||||||
|
<img src="/images/new.png" alt="New" class="badge" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.Genre))
|
||||||
|
{
|
||||||
|
<span class="genre">@main.Metadata.Genre</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(main.Metadata?.ImdbID))
|
||||||
|
{
|
||||||
|
<a class="imdb" href="https://www.imdb.com/title/@main.Metadata.ImdbID" target="_blank">
|
||||||
|
⭐ @main.Metadata.ImdbRating
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-date">
|
||||||
|
<div class="time">@main.PublishDate.ToString("HH:mm")</div>
|
||||||
|
<div class="date">@main.PublishDate.ToString("yyyy-MM-dd")</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-center @(main.Seeders > 40 ? "highlight-green" : "")">@main.Seeders</div>
|
||||||
|
<div class="col-center highlight-red">@main.Leechers</div>
|
||||||
|
<div class="col-action">
|
||||||
|
<form asp-controller="RSS" asp-action="Add" method="post" onsubmit="return confirmDownload('@main.Title')">
|
||||||
|
<input type="hidden" name="rssUrl" value="@main.RssUrl" />
|
||||||
|
<button type="submit" class="btn-add btn-small">➕ Lägg till</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Versioner -->
|
||||||
|
@if (group.Versions.Count > 1)
|
||||||
|
{
|
||||||
|
foreach (var version in group.Versions.Skip(1))
|
||||||
|
{
|
||||||
|
var isLast = version == lastVersion;
|
||||||
|
<div class="rss-row rss-version @(isLast ? "last-version" : "")" title="@version.Title">
|
||||||
|
<div class="col-title">
|
||||||
|
<strong>@version.Title</strong>
|
||||||
|
</div>
|
||||||
|
<div>@version.PublishDate.ToString("HH:mm yyyy-MM-dd")</div>
|
||||||
|
<div class="@(version.Seeders > 40 ? "highlight-green" : "")">@version.Seeders</div>
|
||||||
|
<div class="highlight-red">@version.Leechers</div>
|
||||||
|
<div class="col-action">
|
||||||
|
<form asp-controller="RSS" asp-action="Add" method="post" onsubmit="return confirmDownload('@version.Title')">
|
||||||
|
<input type="hidden" name="rssUrl" value="@version.RssUrl" />
|
||||||
|
<button type="submit" class="btn-add btn-small">➕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="pagination">
|
||||||
|
@for (int i = 1; i <= Model.TotalPages; i++)
|
||||||
|
{
|
||||||
|
if (i == Model.CurrentPage)
|
||||||
|
{
|
||||||
|
<span class="current">@i</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="javascript:void(0)" onclick="loadRss('?page=@i')">@i</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -48,6 +48,31 @@
|
|||||||
{
|
{
|
||||||
<li><a asp-controller="Budget" asp-action="Index"> Budget</a></li>
|
<li><a asp-controller="Budget" asp-action="Index"> Budget</a></li>
|
||||||
}
|
}
|
||||||
|
@if (User.IsInRole("Admin"))
|
||||||
|
{
|
||||||
|
<li>
|
||||||
|
<a asp-controller="Rss" asp-action="Index"> Rss
|
||||||
|
@if (ViewBag.NewTorrentCount > 0)
|
||||||
|
{
|
||||||
|
<span class="new-badge">@ViewBag.NewTorrentCount</span>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (User.IsInRole("Budget"))
|
||||||
|
{
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" role="button" tabindex="0">
|
||||||
|
Rapporter <i class="fas fa-caret-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a asp-controller="report" asp-action="Budget">Budget</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
@if (User.IsInRole("Chef"))
|
@if (User.IsInRole("Chef"))
|
||||||
{
|
{
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
@@ -79,8 +104,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<main class="main-panel">
|
<main class="main-panel">
|
||||||
@RenderBody()
|
@RenderBody()
|
||||||
@RenderSection("Scripts", required: false)
|
@RenderSection("Scripts", required: false)
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
@model TorrentUploadViewModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Torrent Analyzer";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="mb-0">
|
|
||||||
<i class="fas fa-download"></i> Torrent Analyzer
|
|
||||||
</h3>
|
|
||||||
<p class="text-muted mb-0">Ladda upp en torrent-fil för att se seeders/leechers</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if (!ViewData.ModelState.IsValid)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
|
||||||
{
|
|
||||||
<div>@error.ErrorMessage</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form asp-action="Upload" method="post" enctype="multipart/form-data" class="mb-4">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="torrentFile" class="form-label">Välj torrent-fil</label>
|
|
||||||
<input type="file"
|
|
||||||
class="form-control"
|
|
||||||
asp-for="TorrentFile"
|
|
||||||
accept=".torrent"
|
|
||||||
required>
|
|
||||||
<div class="form-text">Endast .torrent filer, max 10MB</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-upload"></i> Analysera Torrent
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@if (Model.ShowResults && Model.TorrentInfo != null)
|
|
||||||
{
|
|
||||||
<hr>
|
|
||||||
<div class="torrent-results">
|
|
||||||
<h4 class="mb-3">
|
|
||||||
<i class="fas fa-info-circle"></i> Torrent Information
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-3"><strong>Filnamn:</strong></div>
|
|
||||||
<div class="col-sm-9">@Model.TorrentInfo.FileName</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-3"><strong>Storlek:</strong></div>
|
|
||||||
<div class="col-sm-9">@FormatFileSize(Model.TorrentInfo.Size)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-3"><strong>Announce URL:</strong></div>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<small class="text-muted font-monospace">@Model.TorrentInfo.AnnounceUrl</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-3"><strong>Info Hash:</strong></div>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<small class="text-muted font-monospace">@Model.TorrentInfo.InfoHash</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.TorrentInfo.ErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
|
||||||
<strong>Tracker-info:</strong> @Model.TorrentInfo.ErrorMessage
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (Model.TorrentInfo.HasTrackerData)
|
|
||||||
{
|
|
||||||
<div class="card bg-light">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="fas fa-chart-bar"></i> Tracker Statistik
|
|
||||||
</h5>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
onclick="refreshStats()"
|
|
||||||
id="refreshBtn">
|
|
||||||
<i class="fas fa-sync-alt"></i> Uppdatera
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row text-center">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-box p-3">
|
|
||||||
<h3 class="text-success mb-1" id="seeders">@Model.TorrentInfo.Seeders</h3>
|
|
||||||
<small class="text-muted">Seeders</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-box p-3">
|
|
||||||
<h3 class="text-warning mb-1" id="leechers">@Model.TorrentInfo.Leechers</h3>
|
|
||||||
<small class="text-muted">Leechers</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-box p-3">
|
|
||||||
<h3 class="text-info mb-1" id="completed">@Model.TorrentInfo.Completed</h3>
|
|
||||||
<small class="text-muted">Completed</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
Tracker-statistik kunde inte hämtas. Detta kan bero på att trackern inte stöder scraping eller kräver autentisering.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@functions {
|
|
||||||
string FormatFileSize(long bytes)
|
|
||||||
{
|
|
||||||
if (bytes == 0) return "Okänd storlek";
|
|
||||||
|
|
||||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
|
||||||
int order = 0;
|
|
||||||
double size = bytes;
|
|
||||||
|
|
||||||
while (size >= 1024 && order < sizes.Length - 1)
|
|
||||||
{
|
|
||||||
order++;
|
|
||||||
size = size / 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{size:0.##} {sizes[order]}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.stat-box {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-results {
|
|
||||||
animation: fadeIn 0.5s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-monospace {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
async function refreshStats() {
|
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
|
||||||
const seedersEl = document.getElementById('seeders');
|
|
||||||
const leechersEl = document.getElementById('leechers');
|
|
||||||
const completedEl = document.getElementById('completed');
|
|
||||||
|
|
||||||
if (!seedersEl) return; // Ingen tracker data att uppdatera
|
|
||||||
|
|
||||||
refreshBtn.disabled = true;
|
|
||||||
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uppdaterar...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('@Url.Action("RefreshStats", "Torrent")', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
infoHash: '@Model.TorrentInfo?.InfoHash',
|
|
||||||
scrapeUrl: '@Model.TorrentInfo?.ScrapeUrl'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
seedersEl.textContent = data.seeders;
|
|
||||||
leechersEl.textContent = data.leechers;
|
|
||||||
completedEl.textContent = data.completed;
|
|
||||||
|
|
||||||
// Visa success animation
|
|
||||||
[seedersEl, leechersEl, completedEl].forEach(el => {
|
|
||||||
el.style.transform = 'scale(1.1)';
|
|
||||||
setTimeout(() => el.style.transform = 'scale(1)', 200);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert('Fel vid uppdatering: ' + (data.error || 'Okänt fel'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fel:', error);
|
|
||||||
alert('Nätverksfel vid uppdatering');
|
|
||||||
} finally {
|
|
||||||
refreshBtn.disabled = false;
|
|
||||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Uppdatera';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -184,3 +184,206 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background: linear-gradient(270deg, #ff6ec4, #7873f5, #4ade80, #facc15, #f87171);
|
||||||
|
background-size: 1000% 1000%;
|
||||||
|
animation: rainbow 6s linear infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.menu-wishlist-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Veckomenyn tar 2/3 av bredden */
|
||||||
|
.weekly-editor {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist {
|
||||||
|
max-width: 100%;
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-header {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-body {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn {
|
||||||
|
border: none;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn.import {
|
||||||
|
background: #4ade80;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn.import:hover {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn.archive {
|
||||||
|
background: #f87171;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn.archive:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wishlist + Recent history till höger */
|
||||||
|
.wishlist-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.recent-history {
|
||||||
|
background: var(--header);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
/* Wrap för horisontell scroll */
|
||||||
|
.recent-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-history-fullwidth table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 600px; /* säkerställer scroll om skärm <600px */
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
/* Mobilanpassning */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.menu-wishlist-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-width: auto; /* låt den krympa */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist {
|
||||||
|
position: static; /* ta bort sticky på mobil */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-actions .btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gör tabeller scrollbara på små skärmar */
|
||||||
|
table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell button {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gör senaste 4 veckor till ett kort på mobil */
|
||||||
|
.recent-history-fullwidth {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
overflow-x: auto; /* egen scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-history-fullwidth table {
|
||||||
|
min-width: 500px; /* så att den triggar scroll */
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-history-fullwidth h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
144
Aberwyn/wwwroot/css/budget-list.css
Normal file
144
Aberwyn/wwwroot/css/budget-list.css
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
:root {
|
||||||
|
--text-main: #1F2937;
|
||||||
|
--text-sub: #64748B;
|
||||||
|
--bg-main: #f8f9fa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--card-income: #4ade80;
|
||||||
|
--card-savings: #facc15;
|
||||||
|
--card-expenses: #f87171;
|
||||||
|
--card-leftover: #fb923c;
|
||||||
|
--card-bar-min-height: 4px; /* minsta höjd för synlighet */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hela sidan */
|
||||||
|
.budget-page {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* År-headers */
|
||||||
|
.year-header {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid för månader (horisontellt med wrap) */
|
||||||
|
.budget-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kort per månad */
|
||||||
|
.budget-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 14px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Månadens namn */
|
||||||
|
.month-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* staplar och text vertikalt centrerade */
|
||||||
|
gap: 6px; /* mellanrum mellan namn och staplar */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-bars {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
height: 30px; /* maxhöjd på staplar */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 6px; /* lite bredare */
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
background-color: gray;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
min-height: 8px; /* alltid synlig höjd */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.bar.income {
|
||||||
|
background-color: var(--card-income);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar.savings {
|
||||||
|
background-color: var(--card-savings);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar.expenses {
|
||||||
|
background-color: var(--card-expenses);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar.leftover {
|
||||||
|
background-color: var(--card-leftover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detaljer */
|
||||||
|
.item-list {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.details-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-toggle input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
155
Aberwyn/wwwroot/css/budget-overview.css
Normal file
155
Aberwyn/wwwroot/css/budget-overview.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/* ===================== Budget Overview CSS ===================== */
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- Column selectors -------- */
|
||||||
|
.budget-column-selector-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-column-selector {
|
||||||
|
position: relative; /* Viktigt för dropdown */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-column-selector span {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-column-selector input[type="text"] {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- Dropdown -------- */
|
||||||
|
.budget-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%; /* Ligger direkt under inputfältet */
|
||||||
|
left: 0;
|
||||||
|
right: 0; /* Matchar inputens bredd */
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 999;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Vertikal lista */
|
||||||
|
padding: 2px 0;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-dropdown-menu label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-dropdown-menu label:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- Spreadsheet -------- */
|
||||||
|
.budget-year-header {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-sheet-header, .budget-sheet-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-sheet-header {
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed main columns */
|
||||||
|
.budget-sheet-cell.month {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-sheet-cell.income,
|
||||||
|
.budget-sheet-cell.expense,
|
||||||
|
.budget-sheet-cell.net {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra columns scrolls horizontally */
|
||||||
|
.budget-extra-columns-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-extra-columns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-sheet-cell.other {
|
||||||
|
flex: 0 0 90px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 4px 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row hover & stripe */
|
||||||
|
.budget-sheet-row:nth-child(even) {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-sheet-row:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Positive/Negative coloring */
|
||||||
|
.budget-positive {
|
||||||
|
color: #014903;
|
||||||
|
font-weight: 470;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-negative {
|
||||||
|
color: #960000;
|
||||||
|
font-weight: 470;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading indicator */
|
||||||
|
.budget-loading-indicator {
|
||||||
|
font-style: italic;
|
||||||
|
color: #aaa;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.budget-total-row {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-month-selector {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-dropdown-menu {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
@@ -97,9 +97,10 @@ body {
|
|||||||
|
|
||||||
.budget-grid {
|
.budget-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -779,3 +780,23 @@ body.dark-mode {
|
|||||||
[ng-cloak] {
|
[ng-cloak] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,3 +323,115 @@ h1 {
|
|||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Flyout popup */
|
||||||
|
.wish-flyout {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px; /* ovanför plustecknet */
|
||||||
|
right: 20px;
|
||||||
|
width: 300px;
|
||||||
|
max-width: 90%;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsiv på mobil */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.wish-flyout {
|
||||||
|
right: 10px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
bottom: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wish-flyout label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wish-flyout input,
|
||||||
|
.wish-flyout textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wish-flyout textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-buttons button[type="submit"] {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-buttons button[type="button"] {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
/* Floating button fixed i nedre högra hörnet */
|
||||||
|
.floating-btn {
|
||||||
|
position: fixed; /* fixed istället för relative/absolute */
|
||||||
|
bottom: 20px; /* avstånd från botten */
|
||||||
|
right: 20px; /* avstånd från höger */
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||||
|
z-index: 1000; /* högre än andra element så den ligger ovanpå */
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-btn:hover {
|
||||||
|
background-color: #2B6CB0;
|
||||||
|
}
|
||||||
|
.autocomplete-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--card-bg);
|
||||||
|
position: absolute;
|
||||||
|
width: 90%; /* Anpassa efter input */
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-list li {
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-list li:hover {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|||||||
25
Aberwyn/wwwroot/css/pizzacalculator.css
Normal file
25
Aberwyn/wwwroot/css/pizzacalculator.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.completed-orders-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-order-box {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 220px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-order-box:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
@@ -465,3 +465,15 @@ body {
|
|||||||
background-color: #e0f0ff;
|
background-color: #e0f0ff;
|
||||||
color: #1F2C3C;
|
color: #1F2C3C;
|
||||||
}
|
}
|
||||||
|
.new-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #ff3b30; /* röd likt Facebooks notis */
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-left: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
194
Aberwyn/wwwroot/css/torrent.css
Normal file
194
Aberwyn/wwwroot/css/torrent.css
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
.torrent-list {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 16px auto;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1F2C3C;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-header, .torrent-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4fr 2fr 1fr 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-header {
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
color: #223344;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-date {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-action {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
width: 28px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Versioner kompakt */
|
||||||
|
.torrent-version {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px; /* Mellanslag mellan titel, datum, seeders, leechers och knapp */
|
||||||
|
padding: 2px 36px;
|
||||||
|
font-size: 11px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.torrent-version .col-title {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: wrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-version .col-date {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-version .col-center {
|
||||||
|
flex: 0.6;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-version .col-action {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-version:hover {
|
||||||
|
background-color: rgba(240, 240, 240, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Huvudrad fortfarande grid */
|
||||||
|
.torrent-row.torrent-group-title {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4fr 2fr 1fr 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.torrent-version.last-version {
|
||||||
|
border-bottom: 1px solid #ccc; /* Streck under sista versionen */
|
||||||
|
}
|
||||||
|
.torrent-row:hover {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
.torrent-row.last-row {
|
||||||
|
border-bottom: 1px solid #ccc; /* streck under sista raden i gruppen */
|
||||||
|
}
|
||||||
|
/* Highlight */
|
||||||
|
.highlight-green {
|
||||||
|
color: #00cc66;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-red {
|
||||||
|
color: #cc3333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-add {
|
||||||
|
background-color: #3399ff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add:hover {
|
||||||
|
background-color: #2389e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Periodval */
|
||||||
|
.torrent-period {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-period button {
|
||||||
|
background: #eee;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-period button.active {
|
||||||
|
background: #3399ff;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a, .pagination span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #eee;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .current {
|
||||||
|
background: #3399ff;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imdb {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a4800;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imdb:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-on {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
BIN
Aberwyn/wwwroot/images/new.png
Normal file
BIN
Aberwyn/wwwroot/images/new.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 863 B |
28
Aberwyn/wwwroot/js/budget-list.js
Normal file
28
Aberwyn/wwwroot/js/budget-list.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
var app = angular.module("budgetListApp", []);
|
||||||
|
app.controller("BudgetListController", function ($scope, $http) {
|
||||||
|
$scope.loading = true;
|
||||||
|
$scope.error = null;
|
||||||
|
$scope.budgets = [];
|
||||||
|
|
||||||
|
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni",
|
||||||
|
"Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||||||
|
|
||||||
|
$scope.getBudgetUrl = function (b) {
|
||||||
|
if (b.name) {
|
||||||
|
return "/budget/" + encodeURIComponent(b.name);
|
||||||
|
}
|
||||||
|
return "/budget/" + b.year + "/" + b.month;
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.get("/api/budget/list")
|
||||||
|
.then(res => {
|
||||||
|
$scope.budgets = res.data;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Kunde inte hämta budgetlista:", err);
|
||||||
|
$scope.error = "Fel vid laddning av budgetar.";
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
$scope.loading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
164
Aberwyn/wwwroot/js/budget-overview.js
Normal file
164
Aberwyn/wwwroot/js/budget-overview.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
angular.module('budgetApp', [])
|
||||||
|
.controller('BudgetOverviewController', ['$scope', '$http', function ($scope, $http) {
|
||||||
|
|
||||||
|
$scope.years = [];
|
||||||
|
$scope.categoryDefColumns = [];
|
||||||
|
$scope.categoryLabelColumns = [];
|
||||||
|
$scope.itemDefColumns = [];
|
||||||
|
$scope.itemLabelColumns = [];
|
||||||
|
|
||||||
|
$scope.selectedCategoryDef = {};
|
||||||
|
$scope.selectedCategoryLabel = {};
|
||||||
|
$scope.selectedItemDef = {};
|
||||||
|
$scope.selectedItemLabel = {};
|
||||||
|
|
||||||
|
$scope.dropdownOpenCatDef = false;
|
||||||
|
$scope.dropdownOpenCatLabel = false;
|
||||||
|
$scope.dropdownOpenItemDef = false;
|
||||||
|
$scope.dropdownOpenItemLabel = false;
|
||||||
|
|
||||||
|
$scope.searchCategoryDef = '';
|
||||||
|
$scope.searchCategoryLabel = '';
|
||||||
|
$scope.searchItemDef = '';
|
||||||
|
$scope.searchItemLabel = '';
|
||||||
|
|
||||||
|
$scope.selectedMonths = {};
|
||||||
|
$scope.dropdownOpenMonth = false;
|
||||||
|
$scope.searchMonth = '';
|
||||||
|
$scope.splitByYear = true; // standard: per år
|
||||||
|
|
||||||
|
|
||||||
|
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||||||
|
$scope.categoryColors = { catDef: {}, catLabel: {} };
|
||||||
|
$scope.columnOrder = { categoryDef: [], categoryLabel: [], itemDef: [], itemLabel: [] };
|
||||||
|
|
||||||
|
$scope.openDropdown = function (type) {
|
||||||
|
$scope.dropdownOpenCatDef = false;
|
||||||
|
$scope.dropdownOpenCatLabel = false;
|
||||||
|
$scope.dropdownOpenItemDef = false;
|
||||||
|
$scope.dropdownOpenItemLabel = false;
|
||||||
|
if (type === 'catDef') $scope.dropdownOpenCatDef = true;
|
||||||
|
if (type === 'catLabel') $scope.dropdownOpenCatLabel = true;
|
||||||
|
if (type === 'itemDef') $scope.dropdownOpenItemDef = true;
|
||||||
|
if (type === 'itemLabel') $scope.dropdownOpenItemLabel = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
const el = document.querySelector('.budget-column-selector-wrapper');
|
||||||
|
if (el && !el.contains(event.target)) {
|
||||||
|
$scope.$apply(function () {
|
||||||
|
$scope.dropdownOpenCatDef = false;
|
||||||
|
$scope.dropdownOpenCatLabel = false;
|
||||||
|
$scope.dropdownOpenItemDef = false;
|
||||||
|
$scope.dropdownOpenItemLabel = false;
|
||||||
|
$scope.dropdownOpenMonth = false;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$http.get('/api/budget/metadata').then(function (metaRes) {
|
||||||
|
// Fyll metadata-kolumner
|
||||||
|
$scope.categoryDefColumns = (metaRes.data.CategoryDefinitions || []).map(c => c.Name);
|
||||||
|
$scope.categoryLabelColumns = (metaRes.data.CategoryLabels || []).map(c => c.Name);
|
||||||
|
$scope.itemDefColumns = (metaRes.data.ItemDefinitions || []).map(c => c.Name);
|
||||||
|
$scope.itemLabelColumns = (metaRes.data.ItemLabels || []).map(c => c.Name);
|
||||||
|
|
||||||
|
// Initiera val
|
||||||
|
$scope.categoryDefColumns.forEach(c => $scope.selectedCategoryDef[c] = false);
|
||||||
|
$scope.categoryLabelColumns.forEach(c => $scope.selectedCategoryLabel[c] = false);
|
||||||
|
$scope.itemDefColumns.forEach(c => $scope.selectedItemDef[c] = false);
|
||||||
|
$scope.itemLabelColumns.forEach(c => $scope.selectedItemLabel[c] = false);
|
||||||
|
|
||||||
|
// Färginställningar
|
||||||
|
(metaRes.data.CategoryDefinitions || []).forEach(c => $scope.categoryColors.catDef[c.Name] = c.Color || '#eee');
|
||||||
|
(metaRes.data.CategoryLabels || []).forEach(c => $scope.categoryColors.catLabel[c.Name] = c.Color || '#eee');
|
||||||
|
|
||||||
|
// Kolumnordning
|
||||||
|
$scope.columnOrder.categoryDef = [...$scope.categoryDefColumns];
|
||||||
|
$scope.columnOrder.categoryLabel = [...$scope.categoryLabelColumns];
|
||||||
|
$scope.columnOrder.itemDef = [...$scope.itemDefColumns];
|
||||||
|
$scope.columnOrder.itemLabel = [...$scope.itemLabelColumns];
|
||||||
|
|
||||||
|
// Hämta rapportdata
|
||||||
|
return $http.get('/api/budget/report/spreadsheet?includeItemDefinitions=true&includeItemLabels=true&includeCategoryDefinitions=true&includeCategoryLabels=true');
|
||||||
|
}).then(function (res) {
|
||||||
|
const sorted = res.data.sort((a, b) => (a.year || 0) - (b.year || 0) || (a.month || 0) - (b.month || 0));
|
||||||
|
|
||||||
|
sorted.forEach(function (month) {
|
||||||
|
['itemDefinitions', 'itemLabels', 'categoryDefinitions', 'categoryLabels'].forEach(function (section) {
|
||||||
|
Object.keys(month[section] || {}).forEach(function (col) {
|
||||||
|
if (section === 'itemDefinitions' && !$scope.selectedItemDef.hasOwnProperty(col)) {
|
||||||
|
$scope.itemDefColumns.push(col);
|
||||||
|
$scope.selectedItemDef[col] = false;
|
||||||
|
$scope.columnOrder.itemDef.push(col);
|
||||||
|
}
|
||||||
|
if (section === 'itemLabels' && !$scope.selectedItemLabel.hasOwnProperty(col)) {
|
||||||
|
$scope.itemLabelColumns.push(col);
|
||||||
|
$scope.selectedItemLabel[col] = false;
|
||||||
|
$scope.columnOrder.itemLabel.push(col);
|
||||||
|
}
|
||||||
|
if (section === 'categoryDefinitions' && !$scope.selectedCategoryDef.hasOwnProperty(col)) {
|
||||||
|
$scope.categoryDefColumns.push(col);
|
||||||
|
$scope.selectedCategoryDef[col] = false;
|
||||||
|
$scope.columnOrder.categoryDef.push(col);
|
||||||
|
}
|
||||||
|
if (section === 'categoryLabels' && !$scope.selectedCategoryLabel.hasOwnProperty(col)) {
|
||||||
|
$scope.categoryLabelColumns.push(col);
|
||||||
|
$scope.selectedCategoryLabel[col] = false;
|
||||||
|
$scope.columnOrder.categoryLabel.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bygg år och månader
|
||||||
|
const yearsMap = {};
|
||||||
|
sorted.forEach(function (m) {
|
||||||
|
if (!yearsMap[m.year]) yearsMap[m.year] = [];
|
||||||
|
yearsMap[m.year].push(m);
|
||||||
|
});
|
||||||
|
$scope.years = Object.keys(yearsMap).map(y => ({ year: parseInt(y), months: yearsMap[y] })).sort((a, b) => b.year - a.year);
|
||||||
|
|
||||||
|
// ✅ Bygg månadsväljaren här
|
||||||
|
$scope.allMonths = [];
|
||||||
|
$scope.years.forEach(y => {
|
||||||
|
y.months.forEach(m => {
|
||||||
|
$scope.allMonths.push({
|
||||||
|
id: `${m.year}-${(m.month < 10 ? '0' : '') + m.month}`,
|
||||||
|
year: m.year,
|
||||||
|
name: $scope.monthNames[m.month - 1]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('Fel vid API-anrop:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$scope.getYearTotal = function (months, section, colName) {
|
||||||
|
return months.reduce(function (sum, m) {
|
||||||
|
if (section === 'income' || section === 'expense' || section === 'net') { return sum + (m[section] || 0); }
|
||||||
|
return sum + ((m[section] && m[section][colName]) || 0);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.filterMonths = function (month) {
|
||||||
|
const anySelected = Object.values($scope.selectedMonths).some(v => v);
|
||||||
|
if (!anySelected) return true; // Visa alla om inget valt
|
||||||
|
const monthId = month.year + '-' + (month.month < 10 ? '0' : '') + month.month;
|
||||||
|
return !!$scope.selectedMonths[monthId];
|
||||||
|
};
|
||||||
|
$scope.$watch('splitByYear', function (newVal) {
|
||||||
|
if (!newVal) {
|
||||||
|
const allMonths = [];
|
||||||
|
$scope.years.forEach(y => allMonths.push(...y.months));
|
||||||
|
$scope.flatYears = [{ year: 'Allt', months: allMonths }];
|
||||||
|
} else {
|
||||||
|
$scope.flatYears = $scope.years;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}]);
|
||||||
@@ -5,6 +5,7 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.error = null;
|
$scope.error = null;
|
||||||
$scope.menuOpen = false;
|
$scope.menuOpen = false;
|
||||||
|
|
||||||
$scope.chartMode = "pie";
|
$scope.chartMode = "pie";
|
||||||
const initialName = window.initialName;
|
const initialName = window.initialName;
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
|
|
||||||
let url = "";
|
let url = "";
|
||||||
if (useName) {
|
if (useName) {
|
||||||
url = `/api/budget/byname/${initialName}`;
|
url = `/api/budget/byname/${encodeURIComponent(initialName)}`;
|
||||||
} else {
|
} else {
|
||||||
url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`;
|
url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`;
|
||||||
}
|
}
|
||||||
@@ -256,6 +257,7 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
isExpense: item.isExpense,
|
isExpense: item.isExpense,
|
||||||
includeInSummary: item.includeInSummary,
|
includeInSummary: item.includeInSummary,
|
||||||
|
budgetItemDefinitionId: item.budgetItemDefinitionId,
|
||||||
budgetCategoryId: category.id,
|
budgetCategoryId: category.id,
|
||||||
order: index
|
order: index
|
||||||
}))
|
}))
|
||||||
@@ -308,6 +310,172 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
$scope.showToast("Fel vid borttagning av månad", true);
|
$scope.showToast("Fel vid borttagning av månad", true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
$scope.deleteBudget = function () {
|
||||||
|
if (!confirm("Vill du verkligen ta bort hela budgeten?")) return;
|
||||||
|
|
||||||
|
if ($scope.budget.name) {
|
||||||
|
// Namnbudget
|
||||||
|
$http.delete(`/api/budget/byname/${encodeURIComponent($scope.budget.name)}`)
|
||||||
|
.then(() => {
|
||||||
|
$scope.showToast("Budget borttagen!");
|
||||||
|
window.location.href = "/budget"; // gå tillbaka till startsida
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Kunde inte ta bort budget:", err);
|
||||||
|
$scope.showToast("Fel vid borttagning", true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Månad
|
||||||
|
const year = $scope.selectedYear;
|
||||||
|
const month = $scope.selectedMonth;
|
||||||
|
$http.delete(`/api/budget/${year}/${month}`)
|
||||||
|
.then(() => {
|
||||||
|
$scope.showToast("Månad borttagen!");
|
||||||
|
$scope.loadBudget();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Kunde inte ta bort månad:", err);
|
||||||
|
$scope.showToast("Fel vid borttagning", true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$scope.selectedBudgetToCopy = null;
|
||||||
|
$scope.formatBudgetName = function (b) {
|
||||||
|
if (b.name) return b.name; // Namnbudget
|
||||||
|
if (b.year && b.month) {
|
||||||
|
// Visa t.ex. "Mars 2025"
|
||||||
|
const monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||||||
|
return monthNames[b.month - 1] + " " + b.year;
|
||||||
|
}
|
||||||
|
return "Okänd budget";
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showCopyModal = false; // styr om modalen syns
|
||||||
|
$scope.budgetList = []; // lista över alla budgeter
|
||||||
|
$scope.selectedBudgetToCopy = null;
|
||||||
|
|
||||||
|
$scope.copyBudget = function () {
|
||||||
|
$scope.loadBudgetList(); // fyller $scope.budgetList
|
||||||
|
$scope.selectedBudgetToCopy = null;
|
||||||
|
$scope.showCopyModal = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$scope.confirmCopyBudget = function () {
|
||||||
|
if (!$scope.selectedBudgetToCopy) {
|
||||||
|
$scope.showToast("Välj en budget att kopiera från", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Förhindra att kopiera till sig själv
|
||||||
|
if ($scope.budget.name && $scope.selectedBudgetToCopy.name &&
|
||||||
|
$scope.budget.name === $scope.selectedBudgetToCopy.name) {
|
||||||
|
$scope.showToast("Du kan inte kopiera en budget till sig själv", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$scope.budget.name && $scope.selectedBudgetToCopy.year &&
|
||||||
|
$scope.selectedBudgetToCopy.month &&
|
||||||
|
$scope.selectedYear === $scope.selectedBudgetToCopy.year &&
|
||||||
|
$scope.selectedMonth === $scope.selectedBudgetToCopy.month) {
|
||||||
|
$scope.showToast("Du kan inte kopiera en budget till sig själv", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
if ($scope.budget.name) {
|
||||||
|
// Aktiv budget = namnbudget
|
||||||
|
url = `/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}`;
|
||||||
|
if ($scope.selectedBudgetToCopy.name) {
|
||||||
|
url += `?from=${encodeURIComponent($scope.selectedBudgetToCopy.name)}`;
|
||||||
|
} else {
|
||||||
|
url += `?fromYear=${$scope.selectedBudgetToCopy.year}&fromMonth=${$scope.selectedBudgetToCopy.month}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Aktiv budget = månad/år
|
||||||
|
const year = $scope.selectedYear;
|
||||||
|
const month = $scope.selectedMonth;
|
||||||
|
url = `/api/budget/copy/${year}/${month}`;
|
||||||
|
if ($scope.selectedBudgetToCopy.name) {
|
||||||
|
url += `?from=${encodeURIComponent($scope.selectedBudgetToCopy.name)}`;
|
||||||
|
} else {
|
||||||
|
url += `?fromYear=${$scope.selectedBudgetToCopy.year}&fromMonth=${$scope.selectedBudgetToCopy.month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Postar till URL:", url);
|
||||||
|
|
||||||
|
$http.post(url)
|
||||||
|
.then(() => {
|
||||||
|
$scope.showToast("Budget kopierad!");
|
||||||
|
$scope.loadBudget();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Kunde inte kopiera budget:", err);
|
||||||
|
$scope.showToast("Fel vid kopiering", true);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
$scope.showCopyModal = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$scope.copyExistingBudget = function () {
|
||||||
|
const from = prompt("Ange namnet på budgeten du vill kopiera från:");
|
||||||
|
if (!from) return;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
if ($scope.budget.name) {
|
||||||
|
// Om det är en namnbudget
|
||||||
|
url = `/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}?from=${encodeURIComponent(from)}`;
|
||||||
|
} else {
|
||||||
|
// Om det är en månadsbudget
|
||||||
|
const year = $scope.selectedYear;
|
||||||
|
const month = $scope.selectedMonth;
|
||||||
|
url = `/api/budget/copy/${year}/${month}?from=${encodeURIComponent(from)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$http.post(url)
|
||||||
|
.then(() => {
|
||||||
|
$scope.showToast("Budget kopierad!");
|
||||||
|
$scope.loadBudget();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Fel vid kopiering av budget:", err);
|
||||||
|
if (err.status === 404) {
|
||||||
|
$scope.showToast("Budgeten du försökte kopiera från finns inte.", true);
|
||||||
|
} else if (err.status === 400) {
|
||||||
|
$scope.showToast("Budgeten har redan data.", true);
|
||||||
|
} else {
|
||||||
|
$scope.showToast("Kunde inte kopiera budget.", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
$scope.createNamedBudget = function () {
|
||||||
|
const name = prompt("Ange namn på din nya budget:");
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
$http.post('/api/budget', { name: name })
|
||||||
|
.then(res => {
|
||||||
|
$scope.showToast("Ny budget skapad!");
|
||||||
|
window.location.href = `/budget/${encodeURIComponent(name)}`;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Fel vid skapande:", err);
|
||||||
|
if (err.status === 409) {
|
||||||
|
$scope.showToast("En budget med detta namn finns redan.", true);
|
||||||
|
} else {
|
||||||
|
$scope.showToast("Kunde inte skapa budget.", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.copyPreviousMonth = function () {
|
$scope.copyPreviousMonth = function () {
|
||||||
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) {
|
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) {
|
||||||
@@ -772,7 +940,9 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$scope.goToBudgetList = function () {
|
||||||
|
$window.location.href = '/budget/list'; // den route som visar listan
|
||||||
|
};
|
||||||
$scope.addItemFromDefinition = function (cat) {
|
$scope.addItemFromDefinition = function (cat) {
|
||||||
const definitionName = cat.newItemDefinition?.trim();
|
const definitionName = cat.newItemDefinition?.trim();
|
||||||
const label = cat.newItemLabel?.trim();
|
const label = cat.newItemLabel?.trim();
|
||||||
@@ -840,27 +1010,36 @@ $scope.addItemFromDefinition = function (cat) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.createEmptyBudget = function () {
|
$scope.createEmptyBudget = function () {
|
||||||
if (!$scope.budget || !$scope.budget.name) {
|
if (!$scope.budget) {
|
||||||
$scope.showToast("Ogiltigt budgetnamn.");
|
$scope.showToast("Ogiltig budget.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto = {
|
let payload = {};
|
||||||
name: $scope.budget.name
|
|
||||||
};
|
|
||||||
|
|
||||||
$http.post('/api/budget', dto)
|
if ($scope.budget.name) {
|
||||||
|
// Namnbudget
|
||||||
|
payload.name = $scope.budget.name;
|
||||||
|
} else {
|
||||||
|
// Månad/år-budget
|
||||||
|
payload.year = $scope.selectedYear;
|
||||||
|
payload.month = $scope.selectedMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
$http.post('/api/budget', payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
$scope.showToast("Ny budget skapad.");
|
$scope.showToast("Ny budget skapad.");
|
||||||
$scope.loadBudget(); // ladda om efter skapandet
|
$scope.loadBudget(); // ladda om efter skapandet
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Fel vid skapande:", error);
|
console.error("Fel vid skapande:", error);
|
||||||
$scope.showToast("Kunde inte skapa budget.");
|
$scope.showToast("Kunde inte skapa budget.", true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$scope.drawCategoryChart = function () {
|
$scope.drawCategoryChart = function () {
|
||||||
const ctx = document.getElementById("expenseChart");
|
const ctx = document.getElementById("expenseChart");
|
||||||
if (!ctx || !$scope.budget?.categories) return;
|
if (!ctx || !$scope.budget?.categories) return;
|
||||||
@@ -973,6 +1152,19 @@ $scope.addItemFromDefinition = function (cat) {
|
|||||||
$scope.importPreview = [];
|
$scope.importPreview = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.budgetList = [];
|
||||||
|
|
||||||
|
$scope.loadBudgetList = function () {
|
||||||
|
return $http.get('/api/budget/list') // eller den endpoint du använder
|
||||||
|
.then(res => {
|
||||||
|
$scope.budgetList = res.data;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Kunde inte hämta budgetlista:", err);
|
||||||
|
$scope.showToast("Fel vid hämtning av budgetlista", true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.loadBudgetList();
|
||||||
|
|
||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
|||||||
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
|
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
|
||||||
|
|
||||||
$scope.loadMenu = function () {
|
$scope.loadMenu = function () {
|
||||||
console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
|
//console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
|
||||||
|
|
||||||
$http.get('/api/mealMenuApi/getWeeklyMenu', {
|
$http.get('/api/mealMenuApi/getWeeklyMenu', {
|
||||||
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
|
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
|
||||||
@@ -28,8 +28,8 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
|||||||
rawMenu.forEach(item => {
|
rawMenu.forEach(item => {
|
||||||
const dayIndex = item.DayOfWeek - 1;
|
const dayIndex = item.DayOfWeek - 1;
|
||||||
const day = $scope.daysOfWeek[dayIndex];
|
const day = $scope.daysOfWeek[dayIndex];
|
||||||
console.warn("Item:", item);
|
//console.warn("Item:", item);
|
||||||
console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
|
//console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
|
||||||
const thumb = item.DinnerMealThumbnail;
|
const thumb = item.DinnerMealThumbnail;
|
||||||
|
|
||||||
$scope.menu[day] = {
|
$scope.menu[day] = {
|
||||||
@@ -39,12 +39,12 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
|||||||
lunchMealName: item.LunchMealName,
|
lunchMealName: item.LunchMealName,
|
||||||
dinnerMealId: item.DinnerMealId,
|
dinnerMealId: item.DinnerMealId,
|
||||||
dinnerMealName: item.DinnerMealName,
|
dinnerMealName: item.DinnerMealName,
|
||||||
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : '/img/default-thumbnail.webp'
|
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
}).catch(err => console.error("Fel vid hämtning av meny:", err));
|
}).catch(err => console.error("Fel vid hämtning av meny:", err));
|
||||||
//$scope.loadSchoolMeals(); // Lägg till här
|
$scope.loadSchoolMeals();
|
||||||
|
|
||||||
};
|
};
|
||||||
$scope.schoolMeals = [];
|
$scope.schoolMeals = [];
|
||||||
@@ -104,7 +104,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.getDayImage = function (day) {
|
$scope.getDayImage = function (day) {
|
||||||
return $scope.menu[day]?.imageUrl || '/img/default-thumbnail.webp';
|
return $scope.menu[day]?.imageUrl || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getMealIdByDay = function (day) {
|
$scope.getMealIdByDay = function (day) {
|
||||||
@@ -152,6 +152,76 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
|||||||
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
|
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
$scope.isLoggedIn = true; // sätt till true/false beroende på användarstatus
|
||||||
|
$scope.showWishForm = false;
|
||||||
|
$scope.showRecipeField = true; // kan styras av autocomplete
|
||||||
|
$scope.newWishName = "";
|
||||||
|
$scope.newWishRecipe = "";
|
||||||
|
|
||||||
|
$scope.showWishFlyout = false;
|
||||||
|
|
||||||
|
$scope.toggleWishFlyout = function () {
|
||||||
|
$scope.showWishFlyout = !$scope.showWishFlyout;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.closeWishFlyout = function () {
|
||||||
|
$scope.showWishFlyout = false;
|
||||||
|
$scope.newWishName = "";
|
||||||
|
$scope.newWishRecipe = "";
|
||||||
|
$scope.showRecipeField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.existingMeals = [];
|
||||||
|
$scope.selectedMeal = null;
|
||||||
|
|
||||||
|
$scope.checkExistingMeal = function () {
|
||||||
|
if (!$scope.newWishName || $scope.newWishName.length < 2) {
|
||||||
|
$scope.existingMeals = [];
|
||||||
|
$scope.showRecipeField = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/MealWish/search?name=' + encodeURIComponent($scope.newWishName))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
$scope.existingMeals = data;
|
||||||
|
$scope.$apply(); // viktigt annars uppdateras inte ng-repeat
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.selectExistingMeal = function (meal) {
|
||||||
|
$scope.newWishName = meal.Name;
|
||||||
|
$scope.selectedMeal = meal;
|
||||||
|
$scope.showRecipeField = true;
|
||||||
|
$scope.existingMeals = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.submitWish = function () {
|
||||||
|
if (!$scope.newWishName) return;
|
||||||
|
if ($scope.existingMeal) {
|
||||||
|
wish.LinkedMealId = $scope.existingMeal.Id;
|
||||||
|
wish.Recipe = $scope.Recipe;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
Name: $scope.newWishName,
|
||||||
|
Recipe: $scope.newWishRecipe
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/MealWish/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log("Önskan skickad:", data);
|
||||||
|
$scope.closeWishFlyout();
|
||||||
|
$scope.$apply();
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log("Initierar meny och måltidsladdning...");
|
console.log("Initierar meny och måltidsladdning...");
|
||||||
$scope.loadMenu();
|
$scope.loadMenu();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const urlsToCache = [
|
|||||||
'/',
|
'/',
|
||||||
'/css/site.css',
|
'/css/site.css',
|
||||||
'/images/lewel-icon.png',
|
'/images/lewel-icon.png',
|
||||||
'/manifest.json'
|
'/manifest-v2.json'
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -14,7 +14,7 @@ self.addEventListener('install', event => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", function (event) {
|
/*self.addEventListener("fetch", function (event) {
|
||||||
const url = new URL(event.request.url);
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
// Hoppa över root / om du inte vill cachea den
|
// Hoppa över root / om du inte vill cachea den
|
||||||
@@ -28,8 +28,29 @@ self.addEventListener("fetch", function (event) {
|
|||||||
return response || fetch(event.request);
|
return response || fetch(event.request);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
});*/
|
||||||
|
|
||||||
|
self.addEventListener("fetch", event => {
|
||||||
|
// 🔴 Ignorera allt som inte är GET (POST, PUT, DELETE etc)
|
||||||
|
if (event.request.method !== "GET") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Hoppa över root om du vill
|
||||||
|
if (url.pathname === "/") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then(response => {
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
self.addEventListener('push', function (event) {
|
self.addEventListener('push', function (event) {
|
||||||
console.log("📨 Push event mottagen!", event);
|
console.log("📨 Push event mottagen!", event);
|
||||||
|
|
||||||
|
|||||||
1
Aberwyn/wwwroot/version.txt
Normal file
1
Aberwyn/wwwroot/version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.0
|
||||||
Reference in New Issue
Block a user